363 lines
24 KiB
JavaScript
363 lines
24 KiB
JavaScript
/* Real DOM exam evidence and submit-state tests. No business scripts or requests. */
|
||
'use strict';
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const { chromium } = require('playwright');
|
||
const rootDir = path.resolve(__dirname, '..');
|
||
const modules = ['question-bank-core.js', 'bank-store.js', 'answer-matcher.js', 'answer-runner.js', 'exam-controller.js'];
|
||
|
||
function paperHTML(total = 25) {
|
||
const questions = Array.from({ length: total }, (_, i) => `<div app-exam-question><div class="pc_examtest_content" data-number="${i + 1}">
|
||
<div class="pc_examtest_content_question"><p>[单选] ${i + 1}. 安全核对题${i + 1}应该怎样操作?<span class="pc_examtest_content_question_value">(${100 / total}分)</span></p><div class="pc_examtest_content_question_difficulty">难度低</div></div>
|
||
<div class="pc_examtest_content_list"><div class="pc_examtest_content_list_des examtest_content_list_des_checked" data-label="A"><span class="examtest_content_list_des_optionsort">A</span>核对措施${i + 1}</div><div class="pc_examtest_content_list_des" data-label="B"><span class="examtest_content_list_des_optionsort">B</span>任意操作${i + 1}</div></div>
|
||
</div></div>`).join('');
|
||
return `<!doctype html><html><head><meta charset="utf-8"><style>body{font:14px system-ui;margin:12px}.pc_examtest_content{padding:3px}.pc_examtest_content_question p{margin:0}.pc_examtest_content_list_des{padding:2px;cursor:pointer}.pc_examtest_content_list_des_checked{background:#ddf2e3}.pc_examtest_commit{padding:12px;background:#ddd;cursor:pointer}.dialog_box{position:fixed;inset:20px auto auto 20px;background:white;border:1px solid;padding:12px}[aria-hidden="true"]{display:none!important}</style></head><body><app-examtest><div class="pc_examtest_title">安全核对考试</div>${questions}<div class="pc_examtest_countdown"><span class="time">00:30:00</span></div><div class="pc_examtest_card">${Array.from({ length: total }, (_, i) => `<div class="examtestcard_count_defult">${i + 1}</div>`).join('')}</div><button class="pc_examtest_commit">交卷</button></app-examtest></body></html>`;
|
||
}
|
||
|
||
(async () => {
|
||
const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true });
|
||
const errors = [], requests = [];
|
||
try {
|
||
const context = await browser.newContext({ viewport: { width: 1000, height: 800 } });
|
||
await context.route('**/*', route => { requests.push(route.request().url()); return route.abort(); });
|
||
const page = await context.newPage();
|
||
page.on('pageerror', error => errors.push(error.message));
|
||
for (const name of modules) await page.addScriptTag({ content: fs.readFileSync(path.join(rootDir, name), 'utf8') });
|
||
|
||
async function setup({ matched = 25, unknownPoint = false, confirm = false, showResult = false, total = 25 } = {}) {
|
||
await page.evaluate(({ html, matched, unknownPoint, confirm, showResult }) => {
|
||
window.qaExam?.destroy(); window.qaRunner?.destroy();
|
||
document.documentElement.innerHTML = html.replace(/^<!doctype html>/, '').replace(/^<html>/, '').replace(/<\/html>$/, '');
|
||
window.qaClock = { now: 0 };
|
||
window.qaSetClock = value => {
|
||
qaClock.now = value;
|
||
const remaining = Math.max(0, 1800 - Math.floor(value / 1000));
|
||
document.querySelector('.pc_examtest_countdown .time').textContent = [Math.floor(remaining / 3600), Math.floor(remaining / 60) % 60, remaining % 60].map(part => String(part).padStart(2, '0')).join(':');
|
||
};
|
||
window.qaStorage = new Map(); window.qaLogs = []; window.qaStates = []; window.qaStorageHook = null; window.qaReadGate = null; window.qaStorageReads = 0;
|
||
window.qaModel = { commitClicks: 0, confirmClicks: 0, optionClicks: [], confirm, showResult };
|
||
window.qaContext = { mode: 'test', exam: true, practice: false, ready: true, local: false,
|
||
sourceUrl: 'http://127.0.0.1/clientcn/exam/qa-paper?flag=test', course: '安全核对考试', revision: 1,
|
||
root: document.querySelector('app-examtest'), routeKey: 'qa-paper', pageSignature: 'qa-stable', topicKey: 'qa-theme' };
|
||
window.qaTopic = { status: 'matched', courses: ['安全知识题库'] };
|
||
const extracted = QuestionBankCore.extract(document);
|
||
window.qaSnapshot = { revision: 1, records: extracted.questions.slice(0, matched).map(question => YianBankStore.validateRecord({
|
||
...question, role: '', course: '安全知识题库', sourceFile: '测试题库.json', answer: 'A', correctTexts: undefined
|
||
})) };
|
||
window.qaRunnerState = 'complete';
|
||
window.qaSettings = { minSubmitMinutes: 6 };
|
||
if (unknownPoint) document.querySelector('.pc_examtest_content_question_value').textContent = '分值待定';
|
||
const boxes = Array.from(document.querySelectorAll('.pc_examtest_content'));
|
||
boxes.forEach((box, index) => box.querySelectorAll('.pc_examtest_content_list_des').forEach(node => node.addEventListener('click', () => {
|
||
box.querySelectorAll('.pc_examtest_content_list_des').forEach(option => option.classList.remove('examtest_content_list_des_checked'));
|
||
node.classList.add('examtest_content_list_des_checked');
|
||
qaModel.optionClicks.push({ number: index + 1, label: node.dataset.label });
|
||
})));
|
||
document.querySelector('.pc_examtest_commit').addEventListener('click', () => {
|
||
qaModel.commitClicks++;
|
||
if (!qaModel.confirm) return;
|
||
const dialog = document.createElement('div'); dialog.className = 'dialog_box';
|
||
dialog.innerHTML = '<div role="dialog"><p>还有未答题,是否确认交卷?</p><button class="qa-cancel">取消</button><button class="qa-confirm">确认交卷</button></div>';
|
||
document.body.appendChild(dialog);
|
||
dialog.querySelector('.qa-confirm').addEventListener('click', () => {
|
||
qaModel.confirmClicks++;
|
||
dialog.remove();
|
||
if (qaModel.showResult) {
|
||
const result = document.createElement('div'); result.className = 'exam_submited_box'; result.textContent = '交卷成功,考试成绩:84分'; document.body.appendChild(result);
|
||
}
|
||
});
|
||
});
|
||
window.qaMakeController = () => YianExamController.create({
|
||
document, now: () => qaClock.now,
|
||
storage: { get: async (key, fallback) => { qaStorageReads++; if (qaReadGate) await qaReadGate; return qaStorage.has(key) ? structuredClone(qaStorage.get(key)) : fallback; },
|
||
set: async (key, value) => { if (qaStorageHook) await qaStorageHook(key, value); qaStorage.set(key, structuredClone(value)); } },
|
||
getContext: () => qaContext, getTopic: () => qaTopic, getSnapshot: () => qaSnapshot,
|
||
getSettings: () => qaSettings, getRunnerState: () => qaRunnerState,
|
||
onState: state => qaStates.push(state), onLog: (level, message) => qaLogs.push({ level, message })
|
||
});
|
||
window.qaExam = qaMakeController();
|
||
}, { html: paperHTML(total), matched, unknownPoint, confirm, showResult });
|
||
return page.evaluate(async () => { await qaExam.observe(); const enabled = await qaExam.enable(); await qaExam.tick(); return { enabled, ...qaExam.getState() }; });
|
||
}
|
||
async function tickAt(seconds) {
|
||
return page.evaluate(async value => { qaSetClock(value * 1000); await qaExam.tick(); return { state: qaExam.getState(), model: qaModel }; }, seconds);
|
||
}
|
||
|
||
let initial = await setup({ matched: 20 });
|
||
assert.equal(initial.total, 25);
|
||
assert.equal(initial.totalScore, 100, '按实际 DOM 分值计算 25×4=100');
|
||
assert.equal(initial.scoreValid, true);
|
||
assert.equal(initial.predictedScore, 80);
|
||
assert.equal(initial.matchedCount, 20);
|
||
assert.equal(initial.missing, 5);
|
||
assert.equal(initial.waitSeconds, 360, 'now 从 0 开始且无 totaltime 参数可记录首次入卷');
|
||
let result = await tickAt(360);
|
||
assert.equal(result.model.commitClicks, 0, '正好 80 分不得自动交卷');
|
||
assert.equal(result.state.stage, 'waiting');
|
||
assert.match(result.state.message, /小于或等于 80/);
|
||
|
||
initial = await setup({ matched: 21 });
|
||
assert.equal(initial.predictedScore, 84);
|
||
assert.equal(initial.verifiedCount, 21);
|
||
assert.equal(initial.missing, 4, '部分题目缺失仍可用已核验题目的实际分值计算');
|
||
result = await tickAt(240);
|
||
assert.equal(result.model.commitClicks, 0, '84 分但只满 4 分钟不得提交');
|
||
assert.equal(result.state.waitSeconds, 120);
|
||
assert.equal(result.state.stage, 'waitingSubmit');
|
||
result = await tickAt(360);
|
||
assert.equal(result.model.commitClicks, 1, '84 分且满 6 分钟点击一次真实交卷入口');
|
||
await tickAt(365);
|
||
assert.equal((await tickAt(372)).model.commitClicks, 1, '提交后等待结果时不得重复点击');
|
||
result = await tickAt(373);
|
||
assert.equal(result.state.stage, 'unknown');
|
||
assert.equal(result.state.enabled, false);
|
||
assert.equal(result.model.commitClicks, 1);
|
||
assert.equal((await tickAt(600)).model.commitClicks, 1, '超过 12 秒未确认后仍禁止重复交卷');
|
||
assert.equal(await page.evaluate(() => qaExam.enable()), false, '未知提交状态不得通过重复启动绕过');
|
||
console.log('PASS 实际分值与部分命中、80 分等待、84 分 4/6 分钟门槛、未知结果有界停止并禁止重复提交');
|
||
|
||
initial = await setup({ matched: 25, unknownPoint: true });
|
||
assert.equal(initial.scoreValid, false);
|
||
assert.match(initial.reason, /分值/);
|
||
assert.equal((await tickAt(600)).model.commitClicks, 0, '分值不明,即使命中全部题目也不得提交');
|
||
await setup({ matched: 21 });
|
||
await page.evaluate(() => { document.querySelector('.pc_examtest_content_list_des').classList.remove('examtest_content_list_des_checked'); });
|
||
result = await tickAt(360);
|
||
assert.equal(result.state.verifiedCount, 20);
|
||
assert.equal(result.state.predictedScore, 80);
|
||
assert.equal(result.model.commitClicks, 0, '匹配记录存在但实际选中内容未回读核验时不计分');
|
||
console.log('PASS 分值未知与实际选中证据变化均阻止自动交卷');
|
||
|
||
await setup({ matched: 21 });
|
||
await page.locator('.pc_examtest_content').first().locator('[data-label="B"]').click();
|
||
result = await tickAt(360);
|
||
assert.equal(result.state.enabled, false);
|
||
assert.equal(result.state.stage, 'paused');
|
||
assert.match(result.state.message, /人工/);
|
||
assert.equal(result.model.commitClicks, 0, '可信人工点击立即取消交卷计划');
|
||
await setup({ matched: 21 });
|
||
await page.evaluate(() => { qaContext.revision = 2; qaSnapshot.revision = 2; });
|
||
result = await tickAt(360);
|
||
assert.equal(result.state.enabled, false);
|
||
assert.equal(result.model.commitClicks, 0, '题库 revision 更新使旧授权失效');
|
||
assert.match(result.state.message, /题库|变更/);
|
||
console.log('PASS 人工作答取消与题库 revision 变化取消旧交卷授权');
|
||
|
||
await setup({ matched: 21, confirm: true, showResult: true });
|
||
result = await tickAt(360);
|
||
assert.equal(result.model.commitClicks, 1);
|
||
assert.equal(result.model.confirmClicks, 0);
|
||
result = await tickAt(361);
|
||
assert.equal(result.model.confirmClicks, 1, '单个真实确认按钮只点击一次');
|
||
result = await tickAt(362);
|
||
assert.equal(result.state.stage, 'submitted');
|
||
assert.equal(result.state.submitted, true, '只有网站结果 DOM 才确认提交成功');
|
||
assert.equal(result.state.enabled, false);
|
||
assert.equal((await tickAt(363)).model.confirmClicks, 1);
|
||
console.log('PASS 交卷确认弹窗及网站成功结果,不从点击动作推断成功');
|
||
|
||
await setup({ matched: 21 });
|
||
await tickAt(240);
|
||
const restored = await page.evaluate(async () => {
|
||
qaExam.destroy(); qaExam = qaMakeController();
|
||
await qaExam.observe(); await qaExam.tick();
|
||
return qaExam.getState();
|
||
});
|
||
assert.equal(restored.elapsedSeconds, 240);
|
||
assert.equal(restored.waitSeconds, 120, '同场重建 controller 复用原始 startedAt=0,计时不归零');
|
||
assert.equal(restored.enabled, false, '刷新恢复仍需明确启动');
|
||
await page.evaluate(() => qaExam.enable());
|
||
assert.equal((await tickAt(360)).model.commitClicks, 1, '同场恢复后仍按原始入卷 6 分钟交卷');
|
||
|
||
await setup({ matched: 21 });
|
||
await page.evaluate(() => {
|
||
qaExam.destroy(); qaStorage.clear(); qaClock.now = 240000;
|
||
qaContext.sourceUrl += '&totaltime=30';
|
||
document.querySelector('.pc_examtest_countdown .time').textContent = '00:26:00';
|
||
qaExam = qaMakeController();
|
||
});
|
||
const inferred = await page.evaluate(async () => { await qaExam.observe(); await qaExam.tick(); return qaExam.getState(); });
|
||
assert.equal(inferred.elapsedSeconds, 240, '首次注入可由明确考试时长和剩余时间反推入卷时间');
|
||
assert.equal(inferred.waitSeconds, 120);
|
||
console.log('PASS 原始入卷时间持久化、0 时间值和明确倒计时反推');
|
||
|
||
for (const mutation of ['ready', 'route', 'revision', 'answers', 'points', 'topic', 'pause']) {
|
||
await setup({ matched: 21 });
|
||
const blocked = await page.evaluate(async mutation => {
|
||
qaSetClock(360000);
|
||
qaStorageHook = async (_key, value) => {
|
||
if (value.stage !== 'submitting') return;
|
||
if (mutation === 'ready') qaContext.ready = false;
|
||
if (mutation === 'route') qaContext.sourceUrl = 'http://127.0.0.1/clientcn/exam/another?flag=test';
|
||
if (mutation === 'revision') { qaContext.revision++; qaSnapshot.revision++; }
|
||
if (mutation === 'answers') document.querySelector('.pc_examtest_content_list_des').classList.remove('examtest_content_list_des_checked');
|
||
if (mutation === 'points') {
|
||
const nodes = document.querySelectorAll('.pc_examtest_content_question_value');
|
||
nodes[0].textContent = '(6分)'; nodes[21].textContent = '(2分)';
|
||
}
|
||
if (mutation === 'topic') qaTopic = { status: 'matched', courses: ['其他主题'] };
|
||
if (mutation === 'pause') qaExam.disable('用户已暂停');
|
||
};
|
||
await qaExam.tick(); return { model: qaModel, state: qaExam.getState() };
|
||
}, mutation);
|
||
assert.equal(blocked.model.commitClicks, 0, '异步保存期间发生 ' + mutation + ' 时禁止旧提交点击');
|
||
assert.equal(blocked.state.enabled, false);
|
||
}
|
||
for (const mutation of ['revision', 'duplicateButton', 'pause']) {
|
||
await setup({ matched: 21, confirm: true });
|
||
await tickAt(360);
|
||
const blocked = await page.evaluate(async mutation => {
|
||
qaStorageHook = async (_key, value) => {
|
||
if (value.stage !== 'confirming') return;
|
||
if (mutation === 'revision') { qaContext.revision++; qaSnapshot.revision++; }
|
||
if (mutation === 'duplicateButton') { const button = document.createElement('button'); button.textContent = '确认交卷'; document.querySelector('.dialog_box').append(button); }
|
||
if (mutation === 'pause') qaExam.disable('用户已暂停');
|
||
};
|
||
await qaExam.tick(); return { model: qaModel, state: qaExam.getState() };
|
||
}, mutation);
|
||
assert.equal(blocked.model.confirmClicks, 0, '二次确认保存期间发生 ' + mutation + ' 时禁止旧确认点击');
|
||
assert.equal(blocked.state.enabled, false);
|
||
}
|
||
console.log('PASS 最后点击前复核:异步保存期间页面、题库、选中集合、主题、暂停及确认入口变化均阻止旧动作');
|
||
|
||
await setup({ matched: 21 });
|
||
const interruptedRead = await page.evaluate(async () => {
|
||
qaExam.destroy(); qaStorageReads = 0;
|
||
let release; qaReadGate = new Promise(resolve => { release = resolve; });
|
||
qaExam = qaMakeController();
|
||
const first = qaExam.observe(), second = qaExam.observe();
|
||
const shared = first === second;
|
||
const enabling = qaExam.enable();
|
||
qaExam.disable('用户暂停'); release();
|
||
const returned = await enabling; await Promise.all([first, second]); qaReadGate = null;
|
||
return { returned, shared, reads: qaStorageReads, state: qaExam.getState() };
|
||
});
|
||
assert.equal(interruptedRead.shared, true, '同场并行 observe 共用一个进行中的 Promise');
|
||
assert.equal(interruptedRead.reads, 1);
|
||
assert.equal(interruptedRead.returned, false);
|
||
assert.equal(interruptedRead.state.enabled, false, 'storage.get 返回后不得重新启用已暂停的考试');
|
||
await setup({ matched: 21 });
|
||
const interruptedWrite = await page.evaluate(async () => {
|
||
let release; const gate = new Promise(resolve => { release = resolve; });
|
||
qaStorageHook = async (_key, value) => { if (value.stage === 'answering') { qaExam.disable('用户停止'); await gate; } };
|
||
const enabling = qaExam.enable();
|
||
release(); return { returned: await enabling, state: qaExam.getState() };
|
||
});
|
||
assert.equal(interruptedWrite.returned, false);
|
||
assert.equal(interruptedWrite.state.enabled, false);
|
||
console.log('PASS observe 共享异步读取及 enable 在读写期间被暂停后不复活');
|
||
|
||
await setup({ matched: 21 });
|
||
const retake = await page.evaluate(async () => {
|
||
qaExam.destroy();
|
||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||
qaStorage.set(key, { ...saved, startedAt: -1200000, stage: 'submitted' });
|
||
qaClock.now = 60000; qaContext.sourceUrl += '&totaltime=30';
|
||
document.querySelector('.pc_examtest_countdown .time').textContent = '00:29:00';
|
||
qaExam = qaMakeController(); await qaExam.observe(); await qaExam.tick();
|
||
const before = qaExam.getState();
|
||
const accepted = await qaExam.enable(); await qaExam.tick();
|
||
return { before, accepted, after: qaExam.getState(), model: qaModel };
|
||
});
|
||
assert.equal(retake.before.submitted, false, '缓存已交卷不能当作当前场次网站成功');
|
||
assert.equal(retake.before.stage, 'awaitingAttempt');
|
||
assert.equal(retake.accepted, true, '明确启动可为当前可作答试卷建立新场次');
|
||
assert.equal(retake.after.elapsedSeconds, 60, '当前30分钟/剩29分钟证据优先,禁止沿用上一场20分钟前的时间');
|
||
assert.equal(retake.after.waitSeconds, 300);
|
||
assert.equal(retake.model.commitClicks, 0);
|
||
assert.equal((await tickAt(360)).model.commitClicks, 1);
|
||
|
||
await setup({ matched: 21 });
|
||
const missingIdentity = await page.evaluate(async () => {
|
||
qaExam.destroy();
|
||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||
qaStorage.set(key, { ...saved, startedAt: -1200000, stage: 'submitted' });
|
||
qaClock.now = 1000000; qaExam = qaMakeController();
|
||
await qaExam.observe(); const accepted = await qaExam.enable(); await qaExam.tick();
|
||
return { accepted, state: qaExam.getState(), model: qaModel };
|
||
});
|
||
assert.equal(missingIdentity.accepted, true);
|
||
assert.equal(missingIdentity.state.elapsedSeconds, 0, '没有明确时长时,新场次从本次明确启动时开始计时');
|
||
assert.equal(missingIdentity.state.waitSeconds, 360);
|
||
assert.equal(missingIdentity.model.commitClicks, 0);
|
||
|
||
await setup({ matched: 21 });
|
||
const equalCountdownRetake = await page.evaluate(async () => {
|
||
qaExam.destroy(); qaClock.now = 360000;
|
||
// Both old checkpoint and new attempt start at 1800. Compare elapsed wall time,
|
||
// not merely whether the new raw value exceeds the old raw value.
|
||
document.querySelector('.pc_examtest_countdown .time').textContent = '00:30:00';
|
||
qaExam = qaMakeController(); await qaExam.observe(); await qaExam.tick();
|
||
const before = qaExam.getState();
|
||
const accepted = await qaExam.enable(); await qaExam.tick();
|
||
return { before, accepted, after: qaExam.getState(), model: qaModel };
|
||
});
|
||
assert.equal(equalCountdownRetake.before.stage, 'awaitingAttempt');
|
||
assert.equal(equalCountdownRetake.accepted, true);
|
||
assert.equal(equalCountdownRetake.after.elapsedSeconds, 0, '旧1800秒检查点六分钟后看到新1800秒,不能算本场已过六分钟');
|
||
assert.equal(equalCountdownRetake.after.waitSeconds, 360);
|
||
assert.equal(equalCountdownRetake.model.commitClicks, 0);
|
||
|
||
await setup({ matched: 21 });
|
||
const noClockEvidence = await page.evaluate(async () => {
|
||
qaExam.destroy(); qaClock.now = 360000;
|
||
document.querySelector('.pc_examtest_countdown').remove();
|
||
qaExam = qaMakeController(); await qaExam.observe(); await qaExam.tick();
|
||
const before = qaExam.getState();
|
||
const accepted = await qaExam.enable(); await qaExam.tick();
|
||
return { before, accepted, after: qaExam.getState(), model: qaModel };
|
||
});
|
||
assert.equal(noClockEvidence.before.stage, 'awaitingAttempt', '没有倒计时或真实场次ID,不能默复用旧时长');
|
||
assert.match(noClockEvidence.before.message, /计时连续性|新场次/);
|
||
assert.equal(noClockEvidence.accepted, true);
|
||
assert.equal(noClockEvidence.after.elapsedSeconds, 0);
|
||
assert.equal(noClockEvidence.after.waitSeconds, 360);
|
||
assert.equal(noClockEvidence.model.commitClicks, 0);
|
||
|
||
await setup({ matched: 21 });
|
||
const pendingAttempt = await page.evaluate(async () => {
|
||
qaExam.destroy();
|
||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||
qaStorage.set(key, { ...saved, stage: 'submitting', submitAt: 0 });
|
||
qaExam = qaMakeController(); await qaExam.observe();
|
||
const blocked = await qaExam.enable(); const stage = qaExam.getState().stage;
|
||
qaContext.sourceUrl += '&attemptId=second-real-attempt';
|
||
const fresh = await qaExam.enable(); await qaExam.tick();
|
||
const queryIsolated = YianExamController.paperKey(qaContext.sourceUrl) !== YianExamController.paperKey(qaContext.sourceUrl.replace('second-real-attempt', 'third-real-attempt'));
|
||
const first = YianExamController.paperKey(qaContext.sourceUrl, qaContext.root);
|
||
qaContext.root.setAttribute('data-attempt-id', 'server-dom-attempt');
|
||
const domIsolated = first !== YianExamController.paperKey(qaContext.sourceUrl, qaContext.root);
|
||
return { blocked, stage, fresh, queryIsolated, domIsolated, model: qaModel };
|
||
});
|
||
assert.equal(pendingAttempt.blocked, false, '未知提交状态禁止按无ID重考自动复用');
|
||
assert.equal(pendingAttempt.stage, 'unknown');
|
||
assert.equal(pendingAttempt.fresh, true, '真实新 attemptId 使用独立场次');
|
||
assert.equal(pendingAttempt.queryIsolated, true);
|
||
assert.equal(pendingAttempt.domIsolated, true);
|
||
assert.equal(pendingAttempt.model.commitClicks, 0);
|
||
console.log('PASS 重考与旧场次隔离:缓存 submitted 不算当前完成、倒计时连续性/无时钟须重确认、未知提交禁止复用及 query/DOM 场次标识');
|
||
|
||
await setup({ total: 3, matched: 3 });
|
||
const interval = await page.evaluate(async () => {
|
||
qaExam.destroy();
|
||
document.querySelectorAll('.pc_examtest_content_list_des').forEach(node => node.classList.remove('examtest_content_list_des_checked'));
|
||
const nativeSetTimeout = window.setTimeout;
|
||
const timers = [];
|
||
window.setTimeout = (callback, delay, ...args) => { timers.push(delay); return nativeSetTimeout(callback, 0, ...args); };
|
||
let runnerState = 'idle';
|
||
qaRunner = YianAnswerRunner.create({ getContext: () => qaContext,
|
||
match: question => YianAnswerMatcher.match(question, { courses: qaTopic.courses }, qaSnapshot), getInterval: () => 4,
|
||
onState: state => { runnerState = state; } });
|
||
try { await qaRunner.start(); return { timers, clicks: qaModel.optionClicks.length, state: runnerState }; }
|
||
finally { window.setTimeout = nativeSetTimeout; qaRunner.destroy(); }
|
||
});
|
||
assert.equal(interval.state, 'complete');
|
||
assert.equal(interval.clicks, 3, 'runner 通过真实 DOM 点击完成三题');
|
||
assert.equal(interval.timers.filter(delay => delay === 4000).length, 2, 'getInterval=4 按相邻题目设置两个 4000ms 间隔');
|
||
assert.equal(requests.length, 0, '整组测试不得请求业务站或其他网络资源');
|
||
assert.deepEqual(errors, [], '测试过程无浏览器脚本错误');
|
||
console.log('PASS 真实 runner 点击与默认 4 秒题间隔;全部测试零网络请求');
|
||
} finally { await browser.close(); }
|
||
})().catch(error => { console.error(error); process.exitCode = 1; });
|