const assert = require('node:assert/strict'); const { chromium } = require('playwright'); const base = process.env.YOUTHOME_WEB_URL || 'http://127.0.0.1:4173/pages'; const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScL/nwAAAABJRU5ErkJggg==', 'base64'); (async () => { const browser = await chromium.launch({ channel: 'msedge', headless: true }); const page = await browser.newPage({ locale: 'zh-CN' }); const calls = []; const errors = []; page.on('pageerror', (error) => errors.push(error.stack || error.message)); await page.addInitScript(() => { localStorage.setItem('youthome:web:dataSource', 'api'); sessionStorage.setItem('youthome:web:accessToken', 'mock-token'); sessionStorage.setItem('youthome:web:authUser', JSON.stringify({ id: 'u1', nickname: '张三', universityName: '测试大学' })); }); await page.route('**/api/**', async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; const isJson = request.headers()['content-type']?.includes('application/json'); calls.push({ method: request.method(), path, body: isJson ? request.postDataJSON() : null }); let data = null; if (path === '/api/auth/captcha') data = null; if (path === '/api/auth/reset-password') data = null; if (path === '/api/ocr/chsi-report') data = { validChsiReport: true, reportId: 'report-1', dedup: { status: 'AVAILABLE' }, name: '张三', idCard: '110101199001011234', school: '测试大学', level: '本科', enrollmentDate: '2022-09' }; if (path === '/api/profile/xuexin/submit') data = { id: 'u1', nickname: '张三', xuexinCertified: true }; if (path === '/api/friendships/search' || path === '/api/groups/search') data = { items: [], total: 0 }; if (path === '/api/campus/activities/search') data = { items: [{ id: 'a1', title: '跑步活动' }], total: 1 }; if (path === '/api/topics/catalog') data = { items: [{ name: '跑步', description: '运动讨论' }], total: 1 }; if (path === '/api/users/privacy') data = { allowStrangerMessage: false, onlineStatusVisible: true }; if (path === '/api/campus/notifications/preferences') data = { pushEnabled: true, systemEnabled: true, socialEnabled: true, activityEnabled: true, chatEnabled: true, quietStart: null, quietEnd: null }; if (path === '/api/campus/activities/joined') data = { items: [], total: 0 }; if (path === '/api/campus/activities/registrations/mine') data = [{ id: 'reg-1', activityId: 'a1', status: 'CONFIRMED', reviewStatus: 'APPROVED', createdAt: '2026-10-22 10:00:00' }]; if (path === '/api/campus/activities/detail') data = { id: 'a1', title: '跑步活动', location: '操场', startTime: '2026-10-23 10:00:00' }; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, message: 'success', data }) }); }); try { await page.goto(`${base}/forgot-password.html`); await page.locator('#panel-step1 input[type="tel"]').fill('13800000000'); await page.locator('#panel-step1 .input-suffix-btn').click(); await page.waitForFunction(() => document.querySelector('#panel-step1 .input-suffix-btn')?.textContent.includes('s')); await page.locator('#panel-step1 input[placeholder*="验证码"]').fill('123456'); await page.locator('#panel-step1 .btn-primary').click(); await page.locator('#panel-step2 input[type="password"]').first().fill('newPass123'); await page.locator('#panel-step2 input[type="password"]').last().fill('wrongPass123'); await page.locator('#panel-step2 .btn-primary').click(); assert.equal(calls.some((call) => call.path === '/api/auth/reset-password'), false); await page.locator('#panel-step2 input[type="password"]').last().fill('newPass123'); await page.locator('#panel-step2 .btn-primary').click(); await page.locator('#panel-step3:visible').waitFor(); assert.deepEqual(calls.find((call) => call.path === '/api/auth/reset-password')?.body, { phone: '13800000000', captcha: '123456', newPassword: 'newPass123' }); await page.goto(`${base}/authentication.html`); await page.locator('#inputRealname').fill('张三'); await page.locator('#inputIdcard').fill('110101199001011234'); await page.locator('#stepPanel1 .btn-primary-large').click(); await page.locator('#fileReport').setInputFiles({ name: 'report.png', mimeType: 'image/png', buffer: png }); await page.locator('#stepPanel2 .btn-primary-large').click(); await page.locator('#stepPanel3.active').waitFor(); await page.locator('#stepPanel3 .checkbox-wrapper').click(); await page.locator('#btnSubmit').click(); await page.locator('#stepPanelSuccess.active').waitFor(); assert.deepEqual(calls.find((call) => call.path === '/api/profile/xuexin/submit')?.body, { reportId: 'report-1' }); await page.goto(`${base}/search.html?q=跑步`); await page.locator('.result-grid .result-card').first().waitFor(); assert.equal(calls.find((call) => call.path === '/api/campus/activities/search')?.method, 'POST'); assert.ok(calls.some((call) => call.path === '/api/topics/catalog')); await page.locator('.search-tab[data-tab="topic"]').click(); await page.locator('.result-grid .result-card').filter({ hasText: '#跑步' }).waitFor(); assert.ok((await page.locator('.result-grid').innerText()).includes('#跑步')); await page.goto(`${base}/settings.html`); await page.locator('[data-settings-section="privacy"]').click(); const stranger = page.locator('.setting-item').filter({ hasText: '允许陌生人私信' }).locator('.switch'); await stranger.waitFor(); await page.waitForFunction(() => document.querySelector('[data-settings-panel="privacy"] .switch')?.getAttribute('aria-checked') !== null); await stranger.click(); await page.waitForFunction(() => document.querySelector('.web-toast')?.textContent.includes('允许陌生人私信已开启')); assert.deepEqual(calls.find((call) => call.path === '/api/users/privacy' && call.method === 'PUT')?.body, { allowStrangerMessage: true }); await page.goto(`${base}/my-activity-apply.html`); await page.locator('.activity-card[data-registration-id="reg-1"]').waitFor(); page.once('dialog', (dialog) => dialog.accept()); await page.locator('.activity-card[data-registration-id="reg-1"] .btn').first().click(); assert.ok(calls.some((call) => call.path === '/api/campus/activities/registrations/reg-1' && call.method === 'DELETE')); assert.deepEqual(errors, []); console.log('api contract smoke passed'); } finally { await browser.close(); } })().catch((error) => { console.error(error); process.exitCode = 1; });