You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

104 lines
6.6 KiB

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; });