fix: deduplicate practice snapshots and compact white panel

This commit is contained in:
suanyuit-cyber
2026-10-29 18:49:42 +08:00
parent 83ca85cc17
commit 762408a347
14 changed files with 1996 additions and 90 deletions
+94 -3
View File
@@ -183,7 +183,7 @@ function bankPage(fetchImpl) {
page.win.addEventListener = events.addEventListener.bind(events);
page.win.removeEventListener = events.removeEventListener.bind(events);
page.win.dispatchEvent = events.dispatchEvent.bind(events);
const state = { sidebar: '判断题库1', player: '判断题库1', exam: '判断题库1', taskId: 'task-a', containerTask: 'task-a', stem: '升温时应选择?', form: true };
const state = { sidebar: '判断题库1', player: '判断题库1', exam: '判断题库1', taskId: 'task-a', containerTask: 'task-a', stem: '升温时应选择?', form: true, questionNumber: 1, progress: '' };
page.win.location.href = 'https://www.shhuayi-elearning.cn/o2o/#/playinfo?taskId=task-a';
page.doc.title = '不会随切换更新的旧标题';
const item = { getAttribute: key => key === 'data-task-id' ? state.taskId : null };
@@ -201,7 +201,7 @@ function bankPage(fetchImpl) {
get textContent() { return state.stem + options.map(option => option.textContent).join(''); },
closest: selector => selector === ROOT_SELECTOR_FOR_TEST ? main : null,
getAttribute: () => null, style: {},
querySelector: selector => selector === '.yxtulcdsdk-form__content' ? { children: [stem] } : null,
querySelector: selector => selector === '.yxtulcdsdk-form__content' ? { children: [stem] } : selector === '.yxtulcdsdk-form__sub' ? { textContent: `${state.questionNumber}.` } : null,
querySelectorAll: () => options
};
page.doc.querySelector = selector => {
@@ -209,6 +209,7 @@ function bankPage(fetchImpl) {
if (selector === 'header.o2oPlayFrame-right .title .yxtf-tooltip') return state.player ? player : null;
if (selector === '.yxtulcdsdk-exam-header .yxtf-tooltip') return state.exam ? exam : null;
if (selector === '.yxtulcdsdk-exam-container[task-id]') return container;
if (selector === '.yxtulcdsdk-answer-card__top') return state.progress ? { textContent: state.progress } : null;
return null;
};
page.doc.querySelectorAll = selector => selector === '.yxtulcdsdk-pc-marking-main .yxtulcdsdk-form' && state.form ? [form] : [];
@@ -343,7 +344,7 @@ test('ordinary pages do not observe requests, capture questions or intercept cop
assert.equal(adapter.isSupportedPage(), false);
assert.equal(adapter.getCurrentQuestion(), null);
assert.equal(adapter.captureCurrent(), null);
assert.deepEqual(adapter.getCurrentBank(), { bankId: '', bankName: '', taskId: '', loading: false });
assert.deepEqual(adapter.getCurrentBank(), { bankId: '', bankName: '', taskId: '', loading: false, questionCount: null });
await page.win.fetch('/upm/start?praId=not-a-bank');
const xhr = new page.win.XMLHttpRequest();
xhr.open('GET', '/upm/start?praId=not-a-bank'); xhr.send(); xhr.respond({ listQues: [single()] });
@@ -408,3 +409,93 @@ test('a lookalike hostname is not treated as the supported learning platform', (
assert.equal(adapter.getCurrentQuestion(), null);
adapter.stop();
});
test('complete snapshots retain repeated content positions and share one batch timestamp', () => {
const raw = { praId: 'p1', listQues: [single(), { ...single(), id: 'q2' }, { id: 'j1', quesType: 2, content: '判断示例', judgeAnswer: 0 }] };
const records = extractQuestions(raw, { batchId: 'batch-a', observedAt: '2026-10-29T09:52:46.000Z' });
assert.equal(records.length, 3, '同批次的两道相同题干是合法两题,不能按内容折成一题');
assert.deepEqual(records.map(q => q.questionIndex), [0, 1, 2]);
assert.deepEqual(records.map(q => q.contentOccurrence), [0, 1, 0]);
assert.equal(new Set(records.map(q => q.source.observedAt)).size, 1);
assert.equal(new Set(records.map(q => q.source.batchId)).size, 1);
});
test('listQues uses the ID submitted by the real practice client, not wrapper identities', () => {
const [record] = extractQuestions({ id: 'list-wrapper', quesType: 2, content: '容器说明', listQues: [{ ...single(), id: 'practice-row-id', quesId: 'bank-item-id', questionId: 'other-wrapper-id' }] });
assert.equal(record.questionId, 'practice-row-id');
assert.equal(record.stem, '升温时应选择?');
});
test('single answer receipts inherit request identity and snapshot occurrence metadata', () => {
const [known] = extractQuestions({ praId: 'p1', listQues: [single()] }, { batchId: 'first-snapshot', observedAt: '2026-10-29T09:52:46.000Z' });
const [answer] = extractQuestions({ id: 'answer-receipt-uuid', answers: ['a2'], explainText: '标准解析' }, {
answerResponse: true, questionId: 'q1', bankId: 'p1', knownQuestions: [known], batchId: 'answer-response'
});
assert.equal(answer.questionId, 'q1');
assert.equal(answer.questionIndex, 0);
assert.equal(answer.contentOccurrence, 0);
assert.equal(answer.source.batchId, 'first-snapshot');
assert.equal(answer.answerStatus, 'confirmed');
assert.deepEqual(extractQuestions({ id: 'receipt', quesType: 0, content: '完全不同的题干', choiceItems: single().choiceItems }, {
answerResponse: true, questionId: 'q1', bankId: 'p1', knownQuestions: [known]
}), [], '不能将另一题正文拼到请求中的题目ID');
});
test('the displayed progress total is available and never carried through a bank transition', () => {
const page = bankPage();
page.state.progress = '进度27/232';
assert.equal(page.adapter.getCurrentBank().questionCount, 232);
page.navigate('单选题库1', 'task-b');
assert.equal(page.adapter.getCurrentBank().questionCount, null, '切库途中旧卡片232不能归入新库');
page.state.player = page.state.exam = '单选题库1'; page.state.containerTask = 'task-b'; page.state.stem = '新题?'; page.state.progress = '进度0/500';
assert.equal(page.adapter.getCurrentBank().questionCount, 500);
page.state.progress = '';
assert.equal(page.adapter.getCurrentBank().questionCount, null);
page.state.progress = '进度233/232';
assert.equal(page.adapter.getCurrentBank().questionCount, null, '畸形进度不作为总题数');
});
test('network snapshots know 232 questions, and late old-bank totals cannot replace the current count', async () => {
const pending = new Map();
const page = bankPage(url => new Promise(resolve => pending.set(url, resolve)));
page.adapter.start();
const first = page.win.fetch('/upm/start?praId=bank-a');
page.navigate('单选题库1', 'task-b');
page.state.player = page.state.exam = '单选题库1'; page.state.containerTask = 'task-b'; page.state.stem = '新题?';
const second = page.win.fetch('/upm/start?praId=bank-b');
const response = (size, prefix) => new Response(JSON.stringify({ listQues: Array.from({ length: size }, (_, i) => ({ ...single(), id: `${prefix}-${i}`, content: `合成题目${i}` })) }), { headers: { 'content-type': 'application/json' } });
pending.get('/upm/start?praId=bank-b')(response(232, 'new'));
await second; await flush(); await flush();
assert.equal(page.adapter.getCurrentBank().questionCount, 232);
pending.get('/upm/start?praId=bank-a')(response(500, 'old'));
await first; await flush(); await flush();
assert.equal(page.adapter.getCurrentBank().questionCount, 232);
const batch = page.received.filter(q => q.bankId === 'bank-b');
assert.equal(batch.length, 232);
assert.equal(new Set(batch.map(q => q.source.batchId)).size, 1);
assert.equal(new Set(batch.map(q => q.source.observedAt)).size, 1);
page.adapter.stop();
});
test('DOM question number distinguishes legitimate identical items in the latest snapshot', async () => {
let prefix = 'old';
const page = bankPage(async () => new Response(JSON.stringify({ listQues: [
{ ...single(), id: `${prefix}-first` }, { ...single(), id: `${prefix}-second` }
] }), { headers: { 'content-type': 'application/json' } }));
page.adapter.start();
await page.win.fetch('/upm/start?praId=bank-a'); await flush(); await flush();
page.state.questionNumber = 2;
const first = page.adapter.getCurrentQuestion();
assert.equal(first.questionId, 'old-second');
assert.equal(first.questionIndex, 1);
assert.equal(first.contentOccurrence, 1);
prefix = 'new';
await page.win.fetch('/upm/start?praId=bank-a'); await flush(); await flush();
const second = page.adapter.getCurrentQuestion();
assert.equal(second.questionId, 'new-second', '刷新完整列表后不再与旧批次的UUID混合匹配');
assert.equal(second.contentOccurrence, 1);
assert.notEqual(second.source.batchId, first.source.batchId);
page.state.questionNumber = 3;
assert.equal(page.adapter.getCurrentQuestion(), null, '题号与已加载完整列表不一致时应等待页面同步');
page.adapter.stop();
});