fix: reduce userscript page blocking and stale option clicks
This commit is contained in:
+90
-2
@@ -275,10 +275,14 @@ function bankPage(fetchImpl) {
|
||||
const player = { get textContent() { return state.player; } };
|
||||
const exam = { get textContent() { return state.exam; } };
|
||||
const container = { getAttribute: key => key === 'task-id' ? state.containerTask : null };
|
||||
const main = { querySelectorAll: () => [] };
|
||||
const main = { querySelectorAll: selector => selector === '.yxtulcdsdk-examing__result-line' ? (state.feedback || []).map(value => ({
|
||||
getAttribute: name => name === 'data-question-id' ? value.questionId || null : null,
|
||||
querySelector: selector => selector === '.yxtulcdsdk-examing__result-line-left' ? { textContent: value.heading } : { textContent: value.answer },
|
||||
closest: () => null
|
||||
})) : [] };
|
||||
const stem = { get textContent() { return state.stem; }, get innerHTML() { return state.stemHTML; }, matches: selector => selector === '[data-rich-text]' };
|
||||
const options = ['降低温度', '提高温度'].map((value, index) => ({
|
||||
querySelector: selector => selector === 'input' ? { value: `a${index + 1}` } : selector === '.yxtf-radio__label, .yxtf-checkbox__label' ? { textContent: `${String.fromCharCode(65 + index)}. ${value}`, querySelector: () => state.optionHTML ? { innerHTML: state.optionHTML[index], textContent: '' } : null } : null,
|
||||
querySelector: selector => selector === 'input' ? { value: state.optionIds?.[index] || `a${index + 1}` } : selector === '.yxtf-radio__label, .yxtf-checkbox__label' ? { textContent: `${String.fromCharCode(65 + index)}. ${value}`, querySelector: () => state.optionHTML ? { innerHTML: state.optionHTML[index], textContent: '' } : null } : null,
|
||||
textContent: value
|
||||
}));
|
||||
const form = {
|
||||
@@ -650,3 +654,87 @@ test('DOM question number distinguishes legitimate identical items in the latest
|
||||
assert.equal(page.adapter.getCurrentQuestion(), null, '题号与已加载完整列表不一致时应等待页面同步');
|
||||
page.adapter.stop();
|
||||
});
|
||||
|
||||
test('hidden parent answer permission cannot be bypassed by nested resultInfo', () => {
|
||||
const raw = single();
|
||||
raw.allowViewSingleQuesAnswer = false;
|
||||
raw.resultInfo = { answers: ['a2'], judgeAnswer: 1 };
|
||||
assert.equal(extractQuestions(raw)[0].answerStatus, 'missing');
|
||||
});
|
||||
|
||||
test('old unbound feedback letters cannot become the next question standard answer', async () => {
|
||||
const next = { ...single(), id: 'q2', content: '另一道题目?' };
|
||||
next.choiceItems.forEach(option => delete option.answer);
|
||||
const page = bankPage(async () => new Response(JSON.stringify({ listQues: [single(), next] }), { headers: { 'content-type': 'application/json' } }));
|
||||
page.adapter.start();
|
||||
await page.win.fetch('/upm/start?praId=bank-a'); await flush(); await flush();
|
||||
page.state.stem = '另一道题目?'; page.state.questionNumber = 2;
|
||||
page.state.feedback = [{ heading: '正确答案', answer: 'A' }, { heading: '解析', answer: '上一题遗留解析' }];
|
||||
const unbound = page.adapter.getCurrentQuestion();
|
||||
assert.equal(unbound.questionId, 'q2');
|
||||
assert.equal(unbound.answerStatus, 'missing');
|
||||
assert.equal(unbound.explanation, '');
|
||||
page.state.feedback = [{ heading: '正确答案', answer: 'A', questionId: 'q1' }];
|
||||
assert.equal(page.adapter.getCurrentQuestion().answerStatus, 'missing', '明确属于上一题的反馈也不能套在新题');
|
||||
page.state.feedback = [{ heading: '正确答案', answer: 'B', questionId: 'q2' }];
|
||||
assert.deepEqual(page.adapter.getCurrentQuestion().answerTexts, ['提高温度']);
|
||||
page.adapter.stop();
|
||||
});
|
||||
|
||||
test('new stem with stale radio option ids waits until DOM values match its network identity', async () => {
|
||||
const raw = { ...single(), id: 'q-new', choiceItems: [
|
||||
{ id: 'new-a', content: '降低温度', answer: 0 }, { id: 'new-b', content: '提高温度', answer: 1 }
|
||||
] };
|
||||
const page = bankPage(async () => new Response(JSON.stringify({ listQues: [raw] }), { headers: { 'content-type': 'application/json' } }));
|
||||
page.adapter.start();
|
||||
await page.win.fetch('/upm/start?praId=bank-a'); await flush(); await flush();
|
||||
assert.equal(page.adapter.getCurrentQuestion(), null, '显示文字一致也不能给旧radio值绑定新题ID');
|
||||
page.state.optionIds = ['new-a', 'new-b'];
|
||||
const question = page.adapter.getCurrentQuestion();
|
||||
assert.equal(question.questionId, 'q-new');
|
||||
assert.deepEqual(question.options.map(option => option.id), ['new-a', 'new-b']);
|
||||
page.adapter.stop();
|
||||
});
|
||||
|
||||
test('observer ignores unrelated and selection mutations and batches relevant work before scanning', () => {
|
||||
const page = bankPage();
|
||||
let callback, reads = 0, sequence = 0;
|
||||
const pending = new Map();
|
||||
page.win.MutationObserver = class { constructor(value) { callback = value; } observe() {} };
|
||||
page.win.setTimeout = fn => { const key = ++sequence; pending.set(key, fn); return key; };
|
||||
page.win.clearTimeout = key => pending.delete(key);
|
||||
const originalOne = page.doc.querySelector;
|
||||
const originalAll = page.doc.querySelectorAll;
|
||||
page.doc.querySelector = selector => { reads++; return originalOne(selector); };
|
||||
page.doc.querySelectorAll = selector => { reads++; return originalAll(selector); };
|
||||
function element(selectors, parentElement = null, id = '') {
|
||||
const result = { nodeType: 1, id, parentElement,
|
||||
matches: selector => selector.split(',').some(part => selectors.includes(part.trim()) || part.trim() === `#${id}`),
|
||||
querySelector: () => null,
|
||||
closest(selector) { return this.matches(selector) ? this : parentElement?.closest(selector) || null; }
|
||||
};
|
||||
return result;
|
||||
}
|
||||
page.adapter.start();
|
||||
const baseline = reads;
|
||||
const plain = element(['.unrelated-timer']);
|
||||
const own = element([], null, 'huayi-qb-root');
|
||||
const main = element([ROOT_SELECTOR_FOR_TEST]);
|
||||
const radio = element(['.yxtulcdsdk-ques-radio'], main);
|
||||
const card = element(['.yxtulcdsdk-answer-card__item']);
|
||||
for (let i = 0; i < 1000; i++) callback([
|
||||
{ type: 'characterData', target: { nodeType: 3, parentElement: plain } },
|
||||
{ type: 'childList', target: own, addedNodes: [], removedNodes: [] },
|
||||
{ type: 'attributes', attributeName: 'class', target: radio },
|
||||
{ type: 'attributes', attributeName: 'class', target: card }
|
||||
]);
|
||||
assert.equal(reads, baseline, '无关页面变化不进行任何全页查询');
|
||||
assert.equal(pending.size, 0, '选中/答题卡/助手自己的变化不调度扫描');
|
||||
const stem = element(['.yxtulcdsdk-form__content'], main);
|
||||
for (let i = 0; i < 100; i++) callback([{ type: 'characterData', target: { nodeType: 3, parentElement: stem } }]);
|
||||
assert.equal(reads, baseline, '相关变化也不在observer回调里同步重扫');
|
||||
assert.equal(pending.size, 1, '连续100次题目变化只调度一次检查');
|
||||
const task = [...pending.values()][0]; pending.clear(); task();
|
||||
assert.ok(reads > baseline, '合批后仍检查真实题目更新');
|
||||
page.adapter.stop();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user