fix: deduplicate practice snapshots and compact white panel
This commit is contained in:
+134
-3
@@ -12,7 +12,8 @@ const http = require('node:http');
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const RECORDS_KEY = 'huayi-qb.records.v1';
|
||||
const CONFIG_KEY = 'huayi-qb.config.v1';
|
||||
const QA_DIR = path.join(os.tmpdir(), 'huayi-qb-qa-v120');
|
||||
const DEDUP_BACKUP_KEY = 'huayi-qb.records.before-dedup.v1';
|
||||
const QA_DIR = path.join(os.tmpdir(), 'huayi-qb-qa-v121');
|
||||
const BUNDLED = path.join(os.homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules');
|
||||
|
||||
function loadPlaywright() {
|
||||
@@ -75,6 +76,26 @@ function bankResponse(bank) {
|
||||
return { data: { praId: bank.id, praName: bank.name, listQues: [{ id: `question-${bank.id}`, quesType: { single: 0, multiple: 1, judge: 2 }[bank.type], content: bank.stem, ...(choiceItems ? { choiceItems } : { judgeAnswer: bank.answer === '正确' ? 1 : 0 }) }] } };
|
||||
}
|
||||
|
||||
const DEDUP_BANK = { id: 'dedup-judge-2', name: '华谊集团化工总控工(高级)-判断题库2' };
|
||||
|
||||
function duplicateJudgeRecords() {
|
||||
// The real 232-question bank contains three pairs with the same wording.
|
||||
// Across three fetches, IDs change, but each original occurrence must remain.
|
||||
return Array.from({ length: 3 }, (_, copy) => Array.from({ length: 232 }, (_, index) => ({
|
||||
bankId: DEDUP_BANK.id, bankName: DEDUP_BANK.name, questionId: `legacy-${index}-${copy}`,
|
||||
type: 'judge', stem: `去重验收第${(index < 229 ? index : index - 229) + 1}项:设备巡检要求已经确认。`,
|
||||
options: [{ id: '1', text: '正确', label: '正确' }, { id: '0', text: '错误', label: '错误' }],
|
||||
correctOptionIds: ['1'], answerTexts: ['正确'], answerStatus: 'confirmed',
|
||||
source: { kind: 'network', url: 'https://api-ulcd.yunxuetang.cn/v2/upm/start', observedAt: `2026-10-29T01:0${copy}:00.000Z` }
|
||||
}))).flat();
|
||||
}
|
||||
|
||||
function duplicateJudgeFixture() {
|
||||
return fixture().replaceAll('华谊集团化工总控工(高级)-判断题库1', DEDUP_BANK.name)
|
||||
.replace('干燥过程传质推动力:物料表面水分压P水>热空气中的水分压P水', duplicateJudgeRecords()[0].stem)
|
||||
.replace('</main>', '<div class="yxtulcdsdk-answer-card__top">进度27/232</div></main>');
|
||||
}
|
||||
|
||||
async function setupBankSwitches(page) {
|
||||
await page.evaluate(banks => {
|
||||
window.__qaBanks = banks;
|
||||
@@ -244,6 +265,12 @@ test('真实浏览器:采集、答案映射、导入导出与面板隐藏恢
|
||||
if (req.url.startsWith('/upm/start') || req.url.startsWith('/unrelated/report')) {
|
||||
const item = new URL(req.url, 'http://127.0.0.1').searchParams.get('item') || '1';
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
if (item === 'legacy-duplicates') {
|
||||
const copy = Math.max(0, Math.min(2, Number(new URL(req.url, 'http://127.0.0.1').searchParams.get('copy')) || 0));
|
||||
res.end(JSON.stringify({ data: { praId: DEDUP_BANK.id, praName: DEDUP_BANK.name,
|
||||
listQues: duplicateJudgeRecords().filter(record => record.questionId.endsWith(`-${copy}`)).map(record => ({ id: record.questionId, quesType: 2, content: record.stem, judgeAnswer: 1 })) } }));
|
||||
return;
|
||||
}
|
||||
if (item.startsWith('bank-')) {
|
||||
const bank = BANK_FIXTURES.find(candidate => item === `bank-${candidate.id}`);
|
||||
const delay = Math.min(1000, Number(new URL(req.url, 'http://127.0.0.1').searchParams.get('delay')) || 0);
|
||||
@@ -271,7 +298,7 @@ test('真实浏览器:采集、答案映射、导入导出与面板隐藏恢
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(req.url.startsWith('/multi-bank') ? multiBankFixture() : markup);
|
||||
res.end(req.url.startsWith('/dedup-bank') ? duplicateJudgeFixture() : req.url.startsWith('/multi-bank') ? multiBankFixture() : markup);
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
@@ -380,7 +407,8 @@ test('真实浏览器:采集、答案映射、导入导出与面板隐藏恢
|
||||
});
|
||||
|
||||
await t.test('JSON导入与当前页面选项乱序:正确内容映射为C', async () => {
|
||||
const initial = (await storageRecords(page))[0];
|
||||
const initial = (await storageRecords(page)).find(record => record.stem.includes('干燥过程传质推动力'));
|
||||
assert.ok(initial, '使用当前页面原始题库作为导入归属,不能依赖记录数组顺序');
|
||||
const record = {
|
||||
bankId: initial.bankId, bankName: initial.bankName, questionId: 'e2e-single-7',
|
||||
type: 'single', stem: '以下哪项操作可以提高温度?',
|
||||
@@ -824,6 +852,109 @@ test('真实浏览器:采集、答案映射、导入导出与面板隐藏恢
|
||||
} finally { await genericContext.close(); }
|
||||
});
|
||||
|
||||
await t.test('紧凑纯白面板与历史三份重复记录修复', async group => {
|
||||
const dedupContext = await browser.newContext({ viewport: { width: 1440, height: 1000 }, acceptDownloads: true });
|
||||
const legacyRecords = duplicateJudgeRecords();
|
||||
await dedupContext.addInitScript({ content: `(${installGMShim.toString()})();\nif(localStorage.getItem(${JSON.stringify(RECORDS_KEY)})===null){GM_setValue(${JSON.stringify(RECORDS_KEY)},${JSON.stringify(legacyRecords)});}\n${script}` });
|
||||
await dedupContext.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.fulfill({ status: 204, body: '' }));
|
||||
const dedupPage = await dedupContext.newPage();
|
||||
dedupPage.on('pageerror', error => errors.push(error.message));
|
||||
dedupPage.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
try {
|
||||
await dedupPage.goto(`${origin}/dedup-bank#/playinfo?taskId=qa-dedup-task`, { waitUntil: 'domcontentloaded' });
|
||||
await dedupPage.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
assert.equal(await dedupPage.locator('#qb-panel').isVisible(), false, '数据迁移不能擅自显示面板');
|
||||
await activate(dedupPage, 'library');
|
||||
|
||||
await group.test('696条历史记录归一为232题,保留三份ID与原始备份', async () => {
|
||||
await waitFor(async () => (await storageRecords(dedupPage)).length === 232, '696条三倍数据应迁移为232道唯一题目');
|
||||
const records = await storageRecords(dedupPage);
|
||||
assert.equal((await dedupPage.locator('#qb-stat-total').innerText()).replace(/,/g, ''), '232');
|
||||
assert.equal((await dedupPage.locator('#qb-stat-confirmed').innerText()).replace(/,/g, ''), '232');
|
||||
assert.match(await dedupPage.locator('#qb-page-total').innerText(), /232/, '页面总数应与平台进度一致');
|
||||
assert.ok(records.every(record => record.questionIdAliases?.length === 3), '每道题应保留三个原始题目ID');
|
||||
assert.equal(new Set(records.map(record => record.stem)).size, 229, '原库同批三对同内容题应保留为232个原始位置,不能压成229');
|
||||
const backup = await dedupPage.evaluate(key => window.GM_getValue(key, null), DEDUP_BACKUP_KEY);
|
||||
const original = Array.isArray(backup) ? backup : backup?.records;
|
||||
assert.equal(original?.length, 696, '首次修复前应保留原696条完整记录');
|
||||
assert.equal(new Set(original.map(record => record.questionId)).size, 696);
|
||||
});
|
||||
|
||||
await group.test('CSV与JSON按唯一题导出,别名保留,旧三份响应不会再次累积', async () => {
|
||||
const exported = await downloadedCSV(dedupPage, 'dedup-232.csv');
|
||||
const parsed = require('../src/core.js').fromCSV(exported.text);
|
||||
assert.equal(parsed.length, 232, 'CSV应只包含232道唯一题,而不是696条记录');
|
||||
assert.ok(parsed.every(record => record.questionIdAliases?.length === 3), 'CSV完整记录JSON应保留全部别名');
|
||||
assert.match(exported.name, /232题/);
|
||||
const jsonWaiting = dedupPage.waitForEvent('download');
|
||||
await dedupPage.locator('[data-action="export-json"]').click();
|
||||
const jsonDownload = await jsonWaiting;
|
||||
const jsonPath = path.join(QA_DIR, 'dedup-232.json');
|
||||
await jsonDownload.saveAs(jsonPath);
|
||||
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
assert.equal(json.records.length, 232);
|
||||
assert.ok(json.records.every(record => record.questionIdAliases?.length === 3));
|
||||
await dedupPage.evaluate(async bankId => {
|
||||
for (const copy of [0, 1, 2]) await fetch(`/upm/start?praId=${bankId}&item=legacy-duplicates©=${copy}`).then(response => response.json());
|
||||
}, DEDUP_BANK.id);
|
||||
await dedupPage.waitForTimeout(550);
|
||||
assert.equal((await storageRecords(dedupPage)).length, 232, '旧的不同ID响应不能让重复题再次长出来');
|
||||
assert.equal((await dedupPage.locator('#qb-stat-total').innerText()).trim(), '232');
|
||||
assert.match(await dedupPage.locator('#qb-page-total').innerText(), /232/, '696条响应不能覆盖DOM中平台总数232');
|
||||
await dedupPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await dedupPage.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
assert.equal(await dedupPage.locator('#qb-panel').isVisible(), false);
|
||||
await activate(dedupPage, 'library');
|
||||
assert.equal((await storageRecords(dedupPage)).length, 232, '刷新后仍应保持唯一题目数');
|
||||
await dedupPage.screenshot({ path: path.join(QA_DIR, 'white-compact-232.png') });
|
||||
});
|
||||
|
||||
await group.test('面板至多300×440,标题按钮与面板均为白色,窄屏无溢出', async () => {
|
||||
await dedupPage.mouse.move(0, 0);
|
||||
const desktop = await dedupPage.locator('#qb-panel').boundingBox();
|
||||
assert.ok(desktop.width <= 301 && desktop.height <= 441, `紧凑面板应至多300×440,实际${desktop.width}×${desktop.height}`);
|
||||
const colors = await dedupPage.locator('#qb-panel').evaluate(panel => ({
|
||||
panel: getComputedStyle(panel).backgroundColor,
|
||||
header: getComputedStyle(panel.querySelector('.header')).backgroundColor,
|
||||
button: getComputedStyle(panel.querySelector('.btn.primary')).backgroundColor
|
||||
}));
|
||||
assert.equal(colors.panel, 'rgb(255, 255, 255)');
|
||||
assert.equal(colors.header, 'rgb(255, 255, 255)');
|
||||
assert.equal(colors.button, 'rgb(255, 255, 255)');
|
||||
await dedupPage.setViewportSize({ width: 390, height: 844 });
|
||||
const mobile = await dedupPage.locator('#qb-panel').boundingBox();
|
||||
assert.ok(mobile.width <= 301 && mobile.height <= 441 && mobile.x >= 0 && mobile.x + mobile.width <= 391);
|
||||
assert.ok(mobile.y >= 0 && mobile.y + mobile.height <= 845);
|
||||
await activate(dedupPage, 'auto');
|
||||
assert.equal(await dedupPage.locator('#qb-auto-enabled').isChecked(), false, '迁移和样式更新不能开启自动答题');
|
||||
await activate(dedupPage, 'library');
|
||||
await dedupPage.screenshot({ path: path.join(QA_DIR, 'white-compact-mobile.png') });
|
||||
await dedupPage.setViewportSize({ width: 1440, height: 1000 });
|
||||
});
|
||||
|
||||
await group.test('冲突重复保留为冲突,不同题库和选项内容不会误合并', async () => {
|
||||
const base = {
|
||||
bankId: DEDUP_BANK.id, bankName: DEDUP_BANK.name, type: 'single', stem: '内容去重边界:应选择哪种操作?',
|
||||
options: [{ id: 'a', text: '甲', label: 'A' }, { id: 'b', text: '乙', label: 'B' }], answerTexts: ['甲'], correctOptionIds: ['a'], answerStatus: 'confirmed'
|
||||
};
|
||||
await importFile(dedupPage, 'dedup-boundaries.json', JSON.stringify({ schemaVersion: 1, records: [
|
||||
{ ...base, questionId: 'conflict-a', source: { kind: 'network', url: 'https://api-ulcd.yunxuetang.cn/v2/upm/start', observedAt: '2026-10-29T02:00:00.000Z' } },
|
||||
{ ...base, questionId: 'conflict-b', answerTexts: ['乙'], correctOptionIds: ['b'], source: { kind: 'network', url: 'https://api-ulcd.yunxuetang.cn/v2/upm/start', observedAt: '2026-10-29T02:01:00.000Z' } },
|
||||
{ ...base, bankId: 'dedup-other-bank', bankName: '另一题库', questionId: 'different-bank' },
|
||||
{ ...base, questionId: 'different-options', options: [{ id: 'a', text: '甲', label: 'A' }, { id: 'c', text: '丙', label: 'B' }] }
|
||||
] }));
|
||||
const records = await storageRecords(dedupPage);
|
||||
const boundary = records.filter(record => record.stem === base.stem);
|
||||
assert.equal(boundary.length, 3, '相同库同内容归一,不同库和不同选项保持独立');
|
||||
const conflicted = boundary.find(record => record.bankId === DEDUP_BANK.id && record.options.some(option => option.text === '乙'));
|
||||
assert.equal(conflicted.answerStatus, 'conflict');
|
||||
assert.ok(conflicted.conflicts?.length >= 2, '两个标准答案应保留供核验');
|
||||
assert.ok(boundary.some(record => record.bankId === 'dedup-other-bank' && record.answerStatus === 'confirmed'));
|
||||
assert.ok(boundary.some(record => record.options.some(option => option.text === '丙')));
|
||||
});
|
||||
} finally { await dedupContext.close(); }
|
||||
});
|
||||
|
||||
await t.test('控制台无脚本错误', async () => { assert.deepEqual(errors, []); });
|
||||
t.diagnostic(`Browser plugin not available; cached Chromium via Playwright. Local origin: ${origin}; viewports 1440x1000 and 390x844; evidence: ${QA_DIR}`);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user