126 lines
9.0 KiB
JavaScript
126 lines
9.0 KiB
JavaScript
'use strict';
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const core = require('../src/core.js');
|
|
const { planAnswer, create } = require('../src/autofill.js');
|
|
|
|
function single(extra = {}) {
|
|
return { bankId: 'bank-a', bankName: '题库一', questionId: 'q1', type: 'single', stem: '以下哪项操作可以提高温度?',
|
|
options: [{ id: 'a', text: '降低温度', label: 'A' }, { id: 'b', text: '提高温度', label: 'B' }, { id: 'c', text: '保持不变', label: 'C' }],
|
|
correctOptionIds: ['b'], answerTexts: ['提高温度'], answerStatus: 'confirmed', ...extra };
|
|
}
|
|
function current(extra = {}) {
|
|
return single({ questionId: '', options: [{ id: 'c', text: '保持不变', label: 'A' }, { id: 'a', text: '降低温度', label: 'B' }, { id: 'b', text: '提高温度', label: 'C' }], answerTexts: [], correctOptionIds: [], answerStatus: 'missing', ...extra });
|
|
}
|
|
|
|
test('single answer plan maps content to the current shuffled choice', () => {
|
|
const plan = planAnswer([single()], current(), core);
|
|
assert.equal(plan.status, 'ready'); assert.deepEqual(plan.labels, ['C']); assert.deepEqual(plan.answerTexts, ['提高温度']);
|
|
});
|
|
test('multiple answer plan preserves the complete shuffled correct set', () => {
|
|
const record = single({ type: 'multiple', answerTexts: ['降低温度', '提高温度'], correctOptionIds: ['a', 'b'] });
|
|
const plan = planAnswer([record], current({ type: 'multiple' }), core);
|
|
assert.equal(plan.status, 'ready'); assert.deepEqual(new Set(plan.labels), new Set(['B', 'C']));
|
|
});
|
|
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: '正确' }] };
|
|
assert.deepEqual(planAnswer([record], question, core).answerTexts, ['错误']);
|
|
assert.deepEqual(planAnswer([record], question, core).labels, ['错误']);
|
|
});
|
|
test('missing and fuzzy-only matches never cause an answer plan', () => {
|
|
assert.equal(planAnswer([single({ answerStatus: 'missing' })], current(), core).status, 'missing');
|
|
assert.equal(planAnswer([single()], current({ stem: '以下哪些操作可以提高温度?' }), core).status, 'missing');
|
|
});
|
|
test('any conflict or different confirmed answers makes the choice ambiguous', () => {
|
|
assert.equal(planAnswer([single(), single({ answerStatus: 'conflict' })], current(), core).status, 'ambiguous');
|
|
assert.equal(planAnswer([single(), single({ questionId: 'q2', correctOptionIds: ['a'], answerTexts: ['降低温度'] })], current(), core).status, 'ambiguous');
|
|
});
|
|
test('duplicate records with the same confirmed answer remain safe', () => {
|
|
assert.equal(planAnswer([single(), single({ questionId: 'q2' })], current(), core).status, 'ready');
|
|
});
|
|
test('a known bank ID rejects answers from other banks', () => {
|
|
assert.equal(planAnswer([single({ bankId: 'bank-b' })], current(), core).status, 'missing');
|
|
assert.equal(planAnswer([single(), single({ bankId: 'bank-b', correctOptionIds: ['a'], answerTexts: ['降低温度'] })], current(), core).status, 'ready');
|
|
});
|
|
test('without bank identity, contradictory cross-bank matches are ambiguous', () => {
|
|
const plan = planAnswer([single(), single({ bankId: 'bank-b', bankName: '题库二', correctOptionIds: ['a'], answerTexts: ['降低温度'] })], current({ bankId: '', bankName: '' }), core);
|
|
assert.equal(plan.status, 'ambiguous');
|
|
});
|
|
test('different complete option sets, duplicate option text and wrong option IDs are refused', () => {
|
|
assert.equal(planAnswer([single()], current({ options: [{ id: 'a', text: '降低温度' }, { id: 'b', text: '提高温度' }] }), core).status, 'missing');
|
|
assert.equal(planAnswer([single()], current({ options: [{ text: '提高温度' }, { text: '提高温度' }] }), core).status, 'ambiguous');
|
|
assert.equal(planAnswer([single()], current({ options: [{ id: 'b', text: '保持不变', label: 'A' }, { id: 'a', text: '降低温度', label: 'B' }, { id: 'c', text: '提高温度', label: 'C' }] }), core).status, 'ambiguous');
|
|
});
|
|
test('normalization preserves negatives, signs, units and superscripts', () => {
|
|
const record = single({ stem: '压力不大于10 MPa,且流量为2 m³/h。' });
|
|
assert.equal(planAnswer([record], current({ stem: '压力大于10 MPa,且流量为2 m³/h。' }), core).status, 'missing');
|
|
assert.equal(planAnswer([record], current({ stem: '压力不大于10 MPa,且流量为2 m3/h。' }), core).status, 'missing');
|
|
assert.equal(planAnswer([record], current({ stem: '压力不大于10 MPa,且流量为2 m³/h。' }), core).status, 'ready');
|
|
});
|
|
test('unsupported, incomplete and malformed questions are skipped', () => {
|
|
assert.equal(planAnswer([], null, core).status, 'unsupported');
|
|
assert.equal(planAnswer([], { stem: '简答题', type: 'unknown' }, core).status, 'unsupported');
|
|
assert.equal(planAnswer([{}, null], current(), core).status, 'missing');
|
|
});
|
|
test('continuous selection starts disabled and can be destroyed without scanning or persistence', () => {
|
|
const calls = [];
|
|
const win = { clearTimeout() {}, clearInterval() {}, setTimeout() { calls.push('timeout'); }, setInterval() { calls.push('interval'); } };
|
|
const engine = create({ window: win, document: {}, core });
|
|
assert.equal(engine.isEnabled(), false);
|
|
assert.equal(engine.getState().answeredCount, 0);
|
|
engine.destroy();
|
|
engine.setEnabled(true);
|
|
assert.equal(engine.isEnabled(), false); assert.deepEqual(calls, []);
|
|
});
|
|
|
|
function checkboxPage({ disabled = false, ariaDisabled = false, duringClick } = {}) {
|
|
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 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,
|
|
click() { clicks.push(index); input.checked = !input.checked; duringClick?.(index); }
|
|
};
|
|
return input;
|
|
});
|
|
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' };
|
|
return { win, doc, record, inputs, clicks, title };
|
|
}
|
|
|
|
test('disabled fieldsets and aria-disabled ancestors are never clicked', async () => {
|
|
for (const config of [{ disabled: true }, { ariaDisabled: true }]) {
|
|
const page = checkboxPage(config);
|
|
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('setEnabled(false) cancels an in-progress one-shot before a second checkbox click', async () => {
|
|
let engine;
|
|
const page = checkboxPage({ duringClick: index => { if (index === 0) engine.setEnabled(false); } });
|
|
engine = create({ window: page.win, document: page.doc, core, getRecords: () => [page.record] });
|
|
const result = await engine.answerCurrent();
|
|
assert.equal(result.status, 'cancelled'); assert.deepEqual(page.clicks, [0]);
|
|
assert.equal(page.inputs[1].checked, false); assert.equal(engine.getState().busy, false);
|
|
engine.destroy();
|
|
});
|
|
test('a question changing during the first click stops the rest of a multiple selection', async () => {
|
|
let page;
|
|
page = checkboxPage({ duringClick: index => { if (index === 0) page.title.textContent = '已经切换到另一题。'; } });
|
|
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, [0]);
|
|
assert.equal(page.inputs[1].checked, false);
|
|
engine.destroy();
|
|
});
|