fix: retain question images and repair evidenced legacy records

This commit is contained in:
suanyuit-cyber
2026-10-29 22:40:38 +08:00
parent 15fa009f9e
commit c3edf3975e
45 changed files with 2778 additions and 103 deletions
+88 -6
View File
@@ -22,6 +22,36 @@ test('multiple answer plan preserves the complete shuffled correct set', () => {
const plan = planAnswer([record], current({ type: 'multiple' }), core);
assert.equal(plan.status, 'ready'); assert.deepEqual(new Set(plan.labels), new Set(['B', 'C']));
});
function repeatedMultiple(extra = {}) {
return single({ type: 'multiple',
options: [{ id: 'a', text: '降低', label: 'A' }, { id: 'b', text: '下降', label: 'B' },
{ id: 'c', text: '增加', label: 'C' }, { id: 'd', text: '下降', label: 'D' }],
correctOptionIds: ['a', 'b', 'c', 'd'], answerTexts: ['降低', '下降', '增加'], ...extra });
}
test('multiple answer plan accepts every explicitly correct occurrence of repeated text', () => {
const record = repeatedMultiple();
const question = repeatedMultiple({ answerStatus: 'missing', correctOptionIds: [], answerTexts: [] });
question.options = [question.options[3], question.options[2], question.options[0], question.options[1]]
.map((option, index) => ({ ...option, id: `new-${index}`, label: 'ABCD'[index] }));
const plan = planAnswer([record], question, core);
assert.equal(plan.status, 'ready');
assert.deepEqual([...plan.labels].sort(), ['A', 'B', 'C', 'D']);
const subset = repeatedMultiple({ correctOptionIds: ['b', 'd'], answerTexts: ['下降'] });
assert.deepEqual(planAnswer([subset], question, core).labels, ['A', 'D']);
});
test('repeated text still skips partial answers, text-only answers, ID contradictions and duplicate labels', () => {
const question = repeatedMultiple({ answerStatus: 'missing', correctOptionIds: [], answerTexts: [] });
assert.equal(planAnswer([repeatedMultiple({ correctOptionIds: ['b'], answerTexts: ['下降'] })], question, core).status, 'ambiguous');
assert.equal(planAnswer([repeatedMultiple({ correctOptionIds: [], answerTexts: ['下降'] })], question, core).status, 'ambiguous');
const changed = repeatedMultiple();
[changed.options[0].id, changed.options[1].id] = [changed.options[1].id, changed.options[0].id];
assert.equal(planAnswer([repeatedMultiple()], changed, core).status, 'ambiguous');
question.options[3].label = 'B';
assert.equal(planAnswer([repeatedMultiple()], question, core).status, 'ambiguous');
});
test('judgement uses explicit correct and incorrect content, including zero ID', () => {
const record = { bankId: 'j', type: 'judge', stem: '这项说法正确吗?', options: [{ id: '1', text: '正确' }, { id: '0', text: '错误' }], correctOptionIds: ['0'], answerStatus: 'confirmed' };
const question = { ...record, options: [{ id: '0', text: '错误', label: '错误' }, { id: '1', text: '正确', label: '正确' }] };
@@ -74,14 +104,15 @@ test('continuous selection starts disabled and can be destroyed without scanning
assert.equal(engine.isEnabled(), false); assert.deepEqual(calls, []);
});
function checkboxPage({ disabled = false, ariaDisabled = false, duringClick } = {}) {
function checkboxPage({ disabled = false, ariaDisabled = false, duringClick, texts = ['检查温度', '检查压力', '跳过巡检'], htmls, stemHTML, richLabels = false } = {}) {
const clicks = [];
const baseNode = () => ({ nodeType: 1, isConnected: true, parentElement: null, hidden: false, getAttribute: () => null, closest: () => null });
const container = { ...baseNode(), matches: () => false, contains: () => true };
const title = { ...baseNode(), textContent: '选择全部正确的巡检操作。', querySelector: () => null };
const inputs = ['检查温度', '检查压力', '跳过巡检'].map((text, index) => {
const label = { ...baseNode(), textContent: text, querySelector: () => null,
cloneNode: () => ({ textContent: text, querySelectorAll: () => [] }) };
const title = { ...baseNode(), textContent: '选择全部正确的巡检操作。', innerHTML: stemHTML, querySelector: () => null };
const inputs = texts.map((text, index) => {
const label = { ...baseNode(), textContent: text,
querySelector: selector => richLabels && selector.includes('[data-rich-text]') ? { innerHTML: htmls?.[index], textContent: text } : null,
cloneNode: () => ({ textContent: text, innerHTML: htmls?.[index], querySelectorAll: () => [] }) };
const input = { ...baseNode(), type: 'checkbox', value: String(index), name: 'checks', id: `check-${index}`, checked: false, disabled: false, labels: [label],
matches: selector => selector === ':disabled' && disabled,
closest: selector => selector === '[aria-disabled="true"]' ? (ariaDisabled ? container : null) : selector === 'label' ? label : selector.startsWith('fieldset') ? container : null,
@@ -92,10 +123,61 @@ function checkboxPage({ disabled = false, ariaDisabled = false, duringClick } =
container.querySelectorAll = selector => selector.startsWith('input') ? inputs : [title];
const doc = { querySelectorAll: selector => selector.startsWith('.yxtulcdsdk') ? [] : [container] };
const win = { getComputedStyle: () => ({ display: 'block', visibility: 'visible' }), clearTimeout() {}, clearInterval() {}, setTimeout() {}, setInterval() {} };
const record = { type: 'multiple', stem: title.textContent, options: inputs.map((input, index) => ({ id: input.value, text: ['检查温度', '检查压力', '跳过巡检'][index] })), answerTexts: ['检查温度', '检查压力'], correctOptionIds: ['0', '1'], answerStatus: 'confirmed' };
const record = { type: 'multiple', stem: title.textContent, options: inputs.map((input, index) => ({ id: input.value, text: texts[index] })), answerTexts: texts.slice(0, 2), correctOptionIds: ['0', '1'], answerStatus: 'confirmed' };
return { win, doc, record, inputs, clicks, title };
}
test('automatic selection chooses all correct repeated checkboxes and clears an incorrect choice', async () => {
const page = checkboxPage({ texts: ['下降', '增加', '下降'] });
page.record.correctOptionIds = ['0', '2']; page.record.answerTexts = ['下降'];
page.inputs[1].checked = true;
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [page.record] });
const result = await engine.answerCurrent();
assert.equal(result.status, 'answered');
assert.deepEqual(page.inputs.map(input => input.checked), [true, false, true]);
assert.deepEqual(page.clicks, [0, 1, 2]);
engine.destroy();
});
test('automatic selection never clicks an ambiguous repeated-text subset', async () => {
const page = checkboxPage({ texts: ['下降', '增加', '下降'] });
page.record.correctOptionIds = ['0']; page.record.answerTexts = ['下降'];
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [page.record] });
const result = await engine.answerCurrent();
assert.equal(result.status, 'skipped'); assert.deepEqual(page.clicks, []);
engine.destroy();
});
test('automatic selection reads image stems and choices consistently with network records', async () => {
const { extractQuestions } = require('../src/adapter.js');
for (const richLabels of [false, true]) {
const stemHTML = '<p>选择对应结构<img src="https://images.example/curve.jpg"></p>';
const htmls = ['<img src="https://images.example/a.png">', '<img src="https://images.example/b.png?x=1&amp;y=2">'];
const [record] = extractQuestions({ id: 'image-single', quesType: 0, content: stemHTML,
choiceItems: htmls.map((itemContent, index) => ({ id: String(index), itemContent })), correctAnswers: ['1'] });
const page = checkboxPage({ texts: ['', ''], htmls, stemHTML, richLabels });
page.inputs.forEach(input => { input.type = 'radio'; });
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [record] });
const result = await engine.answerCurrent();
assert.equal(result.status, 'answered');
assert.deepEqual(result.labels, ['B']);
assert.deepEqual(result.answerTexts, ['[图片:https://images.example/b.png?x=1&y=2]']);
assert.deepEqual(page.clicks, [1]);
engine.destroy();
}
});
test('a different image URL cannot reuse a known image answer', async () => {
const page = checkboxPage({ texts: ['', ''], htmls: ['<img src="https://images.example/a.png">', '<img src="https://images.example/c.png">'] });
const record = single({ type: 'multiple', stem: page.title.textContent,
options: [{ id: '0', text: '[图片:https://images.example/a.png]' }, { id: '1', text: '[图片:https://images.example/b.png]' }],
correctOptionIds: ['1'], answerTexts: ['[图片:https://images.example/b.png]'] });
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [record] });
const result = await engine.answerCurrent();
assert.equal(result.status, 'skipped'); assert.deepEqual(page.clicks, []);
engine.destroy();
});
test('disabled fieldsets and aria-disabled ancestors are never clicked', async () => {
for (const config of [{ disabled: true }, { ariaDisabled: true }]) {
const page = checkboxPage(config);