| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- #!/usr/bin/env node
- // Local production bundle with isolated API fixtures; never touches online lessons.
- import { chromium } from 'playwright';
- import { createServer } from 'node:http';
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
- import { resolve, extname } from 'node:path';
- import assert from 'node:assert/strict';
- const root = resolve(process.env.XIAOSHU_BROWSER_BUILD || 'dist/xiaoshu-mobile/browser');
- const out = resolve(process.env.XIAOSHU_BROWSER_OUTPUT || 'tmp/course-status-cache-qa');
- mkdirSync(out, { recursive: true });
- const server = createServer((req, res) => {
- let file = resolve(root, '.' + decodeURIComponent(new URL(req.url, 'http://local').pathname));
- if (!file.startsWith(root + '/') || !existsSync(file) || !extname(file)) file = root + '/index.html';
- res.setHeader('Content-Type', ({ '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png' })[extname(file)] || 'application/octet-stream');
- res.end(readFileSync(file));
- });
- await new Promise(done => server.listen(0, '127.0.0.1', done));
- const origin = 'http://127.0.0.1:' + server.address().port;
- const today = new Date().toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });
- const statuses = new Map([[901, 0], [902, 0]]), calls = [], errors = [];
- const order = id => ({ GeneralID: id, Title: '测试学员(高中语法写作课)', szyh: 100, pl: 300, plxm: '测试老师', kcid: 0, bindingId: '', courseCategoryKey: 'high_writing', courseCategoryName: '高中语法写作课', durationMinutes: 60, dszt: statuses.get(id), bxrq: today, yysj: today + (id === 901 ? ' 08:00' : ' 09:00'), sdsd: id === 901 ? '08:00' : '09:00', updatedAt: '2026-10-29T00:00:00Z', allowedActions: statuses.get(id) === 0 ? ['start'] : statuses.get(id) === 10 ? ['complete'] : ['editFeedback'], feedback: statuses.get(id) === 30 ? { feedbackStatus: 'pending' } : null });
- const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
- try {
- const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
- await context.addInitScript(() => {
- if (!localStorage.getItem('xiaoshu.angular.session')) localStorage.setItem('xiaoshu.angular.session', JSON.stringify({ token: 'r:isolated-status-fixture', user: { userId: 300, groupId: 3, honeyName: '测试老师' } }));
- });
- await context.route('**/*', async route => {
- const url = new URL(route.request().url());
- if (url.origin !== origin) return route.abort();
- if (!url.pathname.startsWith('/cloud-functions/')) return route.continue();
- const { params: p } = route.request().postDataJSON(); calls.push(p);
- let result = [];
- if (p.action === 'app_companion_overview') result = { courses: [], schedules: [order(901), order(902)], dates: [{ date: today, finished: [...statuses.values()].filter(s => s >= 30).length, planned: [...statuses.values()].filter(s => s < 30).length }], studentOptions: [], page: { itemCount: 2, pageCount: 1 } };
- if (p.action === 'e_order_detail') result = [order(Number(p.id))];
- if (p.action === 'app_live_versions') result = { version: JSON.stringify([...statuses]), scopeVersions: {}, syncStatus: { state: 'current' } };
- if (p.action === 'user_get') result = { userId: 100, groupId: 1, honeyName: '测试学员' };
- if (p.action === 'e_order_update_v2') { statuses.set(Number(JSON.parse(p.content).GeneralID), 10); result = { status: 10 }; }
- if (p.action === 'app_lesson_complete') { statuses.set(Number(p.orderId), 30); result = { status: 30 }; }
- await route.fulfill({ json: { retcode: 0, result } });
- });
- const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message));
- page.on('dialog', dialog => dialog.accept());
- const rows = page.locator('article[data-schedule-id]');
- await page.goto(origin + '/pages/home/shop');
- await rows.filter({ hasText: '未开始' }).nth(1).waitFor();
- // Simulate the old persistent snapshot while the database already has both completions.
- await page.evaluate(() => {
- const saved = JSON.parse(localStorage.getItem('xiaoshu.learning.homeDaily.v1'));
- delete saved.companionCacheVersion;
- localStorage.setItem('xiaoshu.learning.homeDaily.v1', JSON.stringify(saved));
- });
- statuses.set(901, 30); statuses.set(902, 30);
- await page.reload(); await rows.filter({ hasText: '已结课' }).nth(1).waitFor();
- await page.screenshot({ path: out + '/upgraded-cache.png', fullPage: true });
- statuses.set(901, 0); statuses.set(902, 0);
- await page.evaluate(() => localStorage.removeItem('xiaoshu.learning.homeDaily.v1'));
- await page.reload(); await rows.filter({ hasText: '未开始' }).nth(1).waitFor();
- await rows.first().click();
- await page.getByRole('button', { name: '开始课程', exact: true }).click();
- await page.getByRole('button', { name: '结束课程', exact: true }).click();
- await page.locator('.appointment-actions').getByRole('button', { name: '补充反馈', exact: true }).waitFor();
- await page.goBack(); await rows.filter({ hasText: '已结课' }).waitFor();
- assert.equal(await rows.filter({ hasText: '未开始' }).count(), 1);
- // End the second lesson in another tab while the first tab keeps the timetable open.
- const other = await context.newPage(); other.on('dialog', dialog => dialog.accept());
- await other.goto(origin + '/pages/shop/get_order?id=902');
- await other.getByRole('button', { name: '开始课程', exact: true }).click();
- await other.getByRole('button', { name: '结束课程', exact: true }).click();
- await other.locator('.appointment-actions').getByRole('button', { name: '补充反馈', exact: true }).waitFor();
- await rows.filter({ hasText: '已结课' }).nth(1).waitFor();
- await other.close();
- await page.reload(); await rows.filter({ hasText: '已结课' }).nth(1).waitFor();
- await page.screenshot({ path: out + '/completed-after-return.png', fullPage: true });
- assert.deepEqual(errors, []);
- assert.equal(calls.filter(p => p.action === 'app_lesson_complete').length, 2);
- const result = { passed: true, checks: ['旧版当天缓存升级后读取最新已结课状态', '写作课结课返回列表立即更新', '另一标签页结课后打开的课表更新', '整页刷新后两个课次仍显示已结课'], environment: 'local production bundle + isolated API fixtures' };
- writeFileSync(out + '/result.json', JSON.stringify(result, null, 2));
- console.log(JSON.stringify(result));
- } finally { await browser.close(); await new Promise(done => server.close(done)); }
|