feat: add manual confirmation toggle for incomplete exams
This commit is contained in:
+199
-28
@@ -25,8 +25,8 @@ function paperHTML(total = 25) {
|
||||
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 }) => {
|
||||
async function setup({ matched = 25, unknownPoint = false, confirm = false, showResult = false, unansweredWarning = false, manualConfirmIncomplete, total = 25 } = {}) {
|
||||
await page.evaluate(({ html, matched, unknownPoint, confirm, showResult, unansweredWarning, manualConfirmIncomplete }) => {
|
||||
window.qaExam?.destroy(); window.qaRunner?.destroy();
|
||||
document.documentElement.innerHTML = html.replace(/^<!doctype html>/, '').replace(/^<html>/, '').replace(/<\/html>$/, '');
|
||||
window.qaClock = { now: 0 };
|
||||
@@ -36,7 +36,7 @@ function paperHTML(total = 25) {
|
||||
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.qaModel = { commitClicks: 0, confirmClicks: 0, optionClicks: [], confirm, showResult, unansweredWarning };
|
||||
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' };
|
||||
@@ -47,6 +47,7 @@ function paperHTML(total = 25) {
|
||||
})) };
|
||||
window.qaRunnerState = 'complete';
|
||||
window.qaSettings = { minSubmitMinutes: 6 };
|
||||
if (manualConfirmIncomplete !== undefined) qaSettings.manualConfirmIncomplete = manualConfirmIncomplete;
|
||||
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', () => {
|
||||
@@ -58,8 +59,9 @@ function paperHTML(total = 25) {
|
||||
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>';
|
||||
dialog.innerHTML = '<div role="dialog"><p>' + (qaModel.unansweredWarning ? '尚有 1 题未答,是否确认交卷?' : '是否确认交卷?') + '</p><button class="qa-cancel">取消</button><button class="qa-confirm">确认交卷</button></div>';
|
||||
document.body.appendChild(dialog);
|
||||
dialog.querySelector('.qa-cancel').addEventListener('click', () => dialog.remove());
|
||||
dialog.querySelector('.qa-confirm').addEventListener('click', () => {
|
||||
qaModel.confirmClicks++;
|
||||
dialog.remove();
|
||||
@@ -77,7 +79,7 @@ function paperHTML(total = 25) {
|
||||
onState: state => qaStates.push(state), onLog: (level, message) => qaLogs.push({ level, message })
|
||||
});
|
||||
window.qaExam = qaMakeController();
|
||||
}, { html: paperHTML(total), matched, unknownPoint, confirm, showResult });
|
||||
}, { html: paperHTML(total), matched, unknownPoint, confirm, showResult, unansweredWarning, manualConfirmIncomplete });
|
||||
return page.evaluate(async () => { await qaExam.observe(); const enabled = await qaExam.enable(); await qaExam.tick(); return { enabled, ...qaExam.getState() }; });
|
||||
}
|
||||
async function tickAt(seconds) {
|
||||
@@ -95,18 +97,79 @@ function paperHTML(total = 25) {
|
||||
let result = await tickAt(360);
|
||||
assert.equal(result.model.commitClicks, 0, '正好 80 分不得自动交卷');
|
||||
assert.equal(result.state.stage, 'waiting');
|
||||
assert.match(result.state.message, /小于或等于 80/);
|
||||
assert.equal(result.state.requiresManualSubmission, true);
|
||||
assert.match(result.state.message, /人工|手动/);
|
||||
|
||||
initial = await setup({ matched: 21 });
|
||||
assert.equal(initial.predictedScore, 84);
|
||||
assert.equal(initial.verifiedCount, 21);
|
||||
assert.equal(initial.missing, 4, '部分题目缺失仍可用已核验题目的实际分值计算');
|
||||
assert.equal(initial.requiresManualSubmission, true);
|
||||
assert.equal(initial.unverifiedCount, 4);
|
||||
assert.equal(initial.stage, 'waiting', '题库缺题立即转为人工交卷等待');
|
||||
result = await tickAt(240);
|
||||
assert.equal(result.model.commitClicks, 0, '84 分但只满 4 分钟不得提交');
|
||||
assert.equal(result.state.waitSeconds, 120);
|
||||
assert.equal(result.state.stage, 'waiting');
|
||||
result = await tickAt(360);
|
||||
assert.equal(result.model.commitClicks, 0, '84 分且满 6 分钟,缺失 4 题仍须原网页人工交卷');
|
||||
assert.equal(result.model.confirmClicks, 0);
|
||||
assert.match(result.state.message, /人工|手动/);
|
||||
await page.evaluate(async () => { qaExam.disable('用户暂停'); await qaExam.enable(); });
|
||||
result = await tickAt(600);
|
||||
assert.equal(result.model.commitClicks, 0, '重新启用不能作为部分缺题的人工确认或绕过');
|
||||
assert.equal(result.model.confirmClicks, 0);
|
||||
assert.equal(result.state.requiresManualSubmission, true);
|
||||
const replenished = await page.evaluate(async () => {
|
||||
qaSnapshot.records = QuestionBankCore.extract(document).questions.map(question => YianBankStore.validateRecord({
|
||||
...question, role: '', course: '安全知识题库', sourceFile: '补全题库.json', answer: 'A', correctTexts: undefined
|
||||
}));
|
||||
qaSnapshot.revision++; qaContext.revision++;
|
||||
const enabled = await qaExam.enable(); await qaExam.tick();
|
||||
return { enabled, state: qaExam.getState(), model: qaModel };
|
||||
});
|
||||
assert.equal(replenished.enabled, true, '补全题库后仍须明确重新启用');
|
||||
assert.equal(replenished.state.predictedScore, 100);
|
||||
assert.equal(replenished.state.verifiedCount, 25);
|
||||
assert.equal(replenished.state.requiresManualSubmission, false);
|
||||
assert.equal(replenished.model.commitClicks, 1, '新 revision 全卷匹配核验后才恢复正常交卷');
|
||||
console.log('PASS 80 分与 84 分缺题均等待原网页人工交卷,反复启用不能绕过;补全全卷并明确启用可恢复');
|
||||
|
||||
initial = await setup({ matched: 21, manualConfirmIncomplete: false, confirm: true, unansweredWarning: true, showResult: true });
|
||||
assert.equal(initial.predictedScore, 84);
|
||||
assert.equal(initial.requiresManualSubmission, false, '关闭缺题手动确认后恢复部分题自动交卷');
|
||||
assert.equal(initial.stage, 'waitingSubmit');
|
||||
result = await tickAt(240);
|
||||
assert.equal(result.model.commitClicks, 0, '关闭开关后仍须满足 6 分钟');
|
||||
result = await tickAt(360);
|
||||
assert.equal(result.model.commitClicks, 1, '关闭开关后 84 分且满 6 分钟允许交卷');
|
||||
result = await tickAt(361);
|
||||
assert.equal(result.model.confirmClicks, 1, '关闭开关后正常未答提醒可按旧规则确认');
|
||||
result = await tickAt(362);
|
||||
assert.equal(result.state.submitted, true);
|
||||
await setup({ matched: 20, manualConfirmIncomplete: false });
|
||||
result = await tickAt(600);
|
||||
assert.equal(result.state.predictedScore, 80);
|
||||
assert.equal(result.state.stage, 'waiting');
|
||||
assert.equal(result.model.commitClicks, 0, '关闭开关也必须严格大于 80 分');
|
||||
assert.equal(result.model.confirmClicks, 0);
|
||||
await setup({ matched: 22, manualConfirmIncomplete: false });
|
||||
await page.evaluate(() => document.querySelector('.pc_examtest_content_list_des').classList.remove('examtest_content_list_des_checked'));
|
||||
result = await tickAt(600);
|
||||
assert.equal(result.state.verifiedCount, 21);
|
||||
assert.equal(result.state.predictedScore, 84, '高于 80 分也不能跳过其余已匹配题的核验');
|
||||
assert.equal(result.model.commitClicks, 0, '关闭开关仍不能跳过可匹配题的答案全文回读');
|
||||
console.log('PASS 缺题手动确认默认开启;关闭后部分 84 分可交卷和确认,80 分或可匹配题未核验仍禁止');
|
||||
|
||||
initial = await setup();
|
||||
assert.equal(initial.predictedScore, 100);
|
||||
assert.equal(initial.requiresManualSubmission, false);
|
||||
result = await tickAt(240);
|
||||
assert.equal(result.model.commitClicks, 0, '全卷 100 分但只满 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 分钟点击一次真实交卷入口');
|
||||
assert.equal(result.model.commitClicks, 1, '全卷核验 100 分且满 6 分钟点击一次真实交卷入口');
|
||||
await tickAt(365);
|
||||
assert.equal((await tickAt(372)).model.commitClicks, 1, '提交后等待结果时不得重复点击');
|
||||
result = await tickAt(373);
|
||||
@@ -115,28 +178,69 @@ function paperHTML(total = 25) {
|
||||
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 分钟门槛、未知结果有界停止并禁止重复提交');
|
||||
console.log('PASS 全卷覆盖后的 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 setup();
|
||||
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.state.verifiedCount, 24);
|
||||
assert.equal(result.state.predictedScore, 96);
|
||||
assert.equal(result.state.requiresManualSubmission, true);
|
||||
assert.equal(result.state.unverifiedCount, 1);
|
||||
assert.equal(result.state.stage, 'waiting');
|
||||
assert.equal(result.model.commitClicks, 0, '匹配记录存在但实际选中内容未回读核验时不计分');
|
||||
assert.equal(result.model.confirmClicks, 0);
|
||||
console.log('PASS 分值未知与实际选中证据变化均阻止自动交卷');
|
||||
|
||||
await setup({ matched: 21 });
|
||||
for (const incomplete of ['conflict', 'unsupported']) {
|
||||
await setup();
|
||||
const state = await page.evaluate(async incomplete => {
|
||||
if (incomplete === 'conflict') {
|
||||
qaSnapshot.records.push(YianBankStore.validateRecord({ ...qaSnapshot.records[0], id: undefined, answer: 'B', correctTexts: undefined }));
|
||||
} else {
|
||||
document.querySelectorAll('.examtest_content_list_des_optionsort')[1].textContent = 'A';
|
||||
}
|
||||
qaSnapshot.revision++; qaContext.revision++;
|
||||
await qaExam.enable(); qaSetClock(360000); await qaExam.tick();
|
||||
return { state: qaExam.getState(), model: qaModel };
|
||||
}, incomplete);
|
||||
assert.equal(state.state.predictedScore, 96, incomplete + ' 的题目不计入预计得分');
|
||||
assert.equal(state.state.requiresManualSubmission, true, incomplete + ' 也需要人工交卷');
|
||||
assert.equal(state.state.unverifiedCount, 1);
|
||||
assert.equal(state.state.stage, 'waiting');
|
||||
assert.equal(state.model.commitClicks, 0);
|
||||
assert.equal(state.model.confirmClicks, 0);
|
||||
assert.ok(state.state.details.some(item => item.status === incomplete));
|
||||
}
|
||||
for (const manualConfirmIncomplete of [true, false]) for (const incomplete of ['missingCard', 'missingCardEntry', 'missingQuestion']) {
|
||||
await setup({ manualConfirmIncomplete });
|
||||
const blocked = await page.evaluate(async incomplete => {
|
||||
if (incomplete === 'missingCard') document.querySelector('.pc_examtest_card').remove();
|
||||
if (incomplete === 'missingCardEntry') document.querySelector('.examtestcard_count_defult').remove();
|
||||
if (incomplete === 'missingQuestion') document.querySelector('[app-exam-question]').remove();
|
||||
const enabled = await qaExam.enable(); qaSetClock(360000); await qaExam.tick();
|
||||
return { enabled, state: qaExam.getState(), model: qaModel };
|
||||
}, incomplete);
|
||||
assert.equal(blocked.enabled, false, incomplete + ' 在开关为 ' + manualConfirmIncomplete + ' 时不允许将部分 DOM 当作完整试卷启动');
|
||||
assert.equal(blocked.state.complete, false);
|
||||
assert.equal(blocked.state.scoreValid, false);
|
||||
assert.equal(blocked.model.commitClicks, 0);
|
||||
assert.equal(blocked.model.confirmClicks, 0);
|
||||
}
|
||||
console.log('PASS 冲突、不支持、未全文核验及答题卡/题目 DOM 不完整均不能自动交卷');
|
||||
|
||||
await setup();
|
||||
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.match(result.state.message, /人工|手动/);
|
||||
assert.equal(result.model.commitClicks, 0, '可信人工点击立即取消交卷计划');
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
await page.evaluate(() => { qaContext.revision = 2; qaSnapshot.revision = 2; });
|
||||
result = await tickAt(360);
|
||||
assert.equal(result.state.enabled, false);
|
||||
@@ -144,7 +248,7 @@ function paperHTML(total = 25) {
|
||||
assert.match(result.state.message, /题库|变更/);
|
||||
console.log('PASS 人工作答取消与题库 revision 变化取消旧交卷授权');
|
||||
|
||||
await setup({ matched: 21, confirm: true, showResult: true });
|
||||
await setup({ confirm: true, showResult: true });
|
||||
result = await tickAt(360);
|
||||
assert.equal(result.model.commitClicks, 1);
|
||||
assert.equal(result.model.confirmClicks, 0);
|
||||
@@ -157,7 +261,60 @@ function paperHTML(total = 25) {
|
||||
assert.equal((await tickAt(363)).model.confirmClicks, 1);
|
||||
console.log('PASS 交卷确认弹窗及网站成功结果,不从点击动作推断成功');
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup({ confirm: true, unansweredWarning: true, showResult: true });
|
||||
result = await tickAt(360);
|
||||
assert.equal(result.model.commitClicks, 1, '本地完整核验后可点击初始交卷入口');
|
||||
result = await tickAt(361);
|
||||
assert.equal(result.model.confirmClicks, 0, '网站明确提示未答题时必须让用户人工确认');
|
||||
assert.equal(result.state.requiresManualSubmission, true);
|
||||
assert.equal(result.state.stage, 'waiting');
|
||||
assert.equal(result.state.enabled, false);
|
||||
assert.match(result.state.message, /人工|手动/);
|
||||
assert.equal(await page.evaluate(() => qaExam.enable()), false, '网站未答题提醒不得通过重新启动绕过');
|
||||
assert.equal((await tickAt(600)).model.confirmClicks, 0);
|
||||
const restoredManualWarning = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaExam = qaMakeController();
|
||||
await qaExam.observe(); const enabled = await qaExam.enable(); await qaExam.tick();
|
||||
return { enabled, state: qaExam.getState(), model: qaModel };
|
||||
});
|
||||
assert.equal(restoredManualWarning.enabled, false, '网站未答提醒在 controller 重建后仍禁止自动确认');
|
||||
assert.equal(restoredManualWarning.state.requiresManualSubmission, true);
|
||||
assert.equal(restoredManualWarning.state.stage, 'waiting');
|
||||
assert.equal(restoredManualWarning.model.commitClicks, 1);
|
||||
assert.equal(restoredManualWarning.model.confirmClicks, 0);
|
||||
await page.locator('.qa-confirm').click();
|
||||
result = await tickAt(601);
|
||||
assert.equal(result.model.confirmClicks, 1, '用户仍能在网站弹窗完成真实人工确认');
|
||||
assert.equal(result.state.submitted, true);
|
||||
console.log('PASS 网站未答题提醒优先于本地全卷核验,自动确认停止且只能原网页人工确认');
|
||||
|
||||
await setup({ matched: 25, confirm: true, unansweredWarning: true, showResult: true });
|
||||
await tickAt(360); await tickAt(361);
|
||||
const unlockedWarning = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaExam = qaMakeController(); await qaExam.observe();
|
||||
const before = await qaExam.enable();
|
||||
qaSettings.manualConfirmIncomplete = false;
|
||||
qaExam.disable('设置已变更,请明确继续');
|
||||
await qaExam.tick(); const blocked = qaModel.confirmClicks;
|
||||
const withDialog = await qaExam.enable(); await qaExam.tick();
|
||||
return { before, blocked, withDialog, state: qaExam.getState(), model: qaModel };
|
||||
});
|
||||
assert.equal(unlockedWarning.before, false, '已持久化的网站未答提醒在默认开启时仍阻止自动确认');
|
||||
assert.equal(unlockedWarning.blocked, 0, '切换开关本身不会自动点击确认');
|
||||
assert.equal(unlockedWarning.withDialog, false, '旧网站弹窗仍打开时拒绝重新启用或接管');
|
||||
assert.equal(unlockedWarning.model.confirmClicks, 0);
|
||||
assert.equal(unlockedWarning.model.commitClicks, 1, '旧弹窗仍在时不能重复点击初始交卷入口');
|
||||
await page.locator('.qa-cancel').click();
|
||||
const newPlan = await page.evaluate(async () => { const enabled = await qaExam.enable(); await qaExam.tick(); return { enabled, model: qaModel }; });
|
||||
assert.equal(newPlan.enabled, true, '用户先取消原网页弹窗再明确继续可按关闭后的策略重新核验');
|
||||
assert.equal(newPlan.model.commitClicks, 2, '新明确启动的计划可重新发起一次交卷');
|
||||
assert.equal(newPlan.model.confirmClicks, 0);
|
||||
result = await tickAt(362);
|
||||
assert.equal(result.model.confirmClicks, 1);
|
||||
assert.equal((await tickAt(363)).state.submitted, true);
|
||||
console.log('PASS 网站提醒锁定恢复后,关闭开关→取消旧弹窗→明确继续,才重新核验并发起新交卷');
|
||||
|
||||
await setup();
|
||||
await tickAt(240);
|
||||
const restored = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaExam = qaMakeController();
|
||||
@@ -170,7 +327,7 @@ function paperHTML(total = 25) {
|
||||
await page.evaluate(() => qaExam.enable());
|
||||
assert.equal((await tickAt(360)).model.commitClicks, 1, '同场恢复后仍按原始入卷 6 分钟交卷');
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
await page.evaluate(() => {
|
||||
qaExam.destroy(); qaStorage.clear(); qaClock.now = 240000;
|
||||
qaContext.sourceUrl += '&totaltime=30';
|
||||
@@ -182,8 +339,8 @@ function paperHTML(total = 25) {
|
||||
assert.equal(inferred.waitSeconds, 120);
|
||||
console.log('PASS 原始入卷时间持久化、0 时间值和明确倒计时反推');
|
||||
|
||||
for (const mutation of ['ready', 'route', 'revision', 'answers', 'points', 'topic', 'pause']) {
|
||||
await setup({ matched: 21 });
|
||||
for (const mutation of ['ready', 'route', 'revision', 'answers', 'points', 'topic', 'pause', 'manualConfirmIncomplete']) {
|
||||
await setup();
|
||||
const blocked = await page.evaluate(async mutation => {
|
||||
qaSetClock(360000);
|
||||
qaStorageHook = async (_key, value) => {
|
||||
@@ -198,14 +355,15 @@ function paperHTML(total = 25) {
|
||||
}
|
||||
if (mutation === 'topic') qaTopic = { status: 'matched', courses: ['其他主题'] };
|
||||
if (mutation === 'pause') qaExam.disable('用户已暂停');
|
||||
if (mutation === 'manualConfirmIncomplete') qaSettings.manualConfirmIncomplete = false;
|
||||
};
|
||||
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 });
|
||||
for (const mutation of ['revision', 'duplicateButton', 'pause', 'manualConfirmIncomplete']) {
|
||||
await setup({ confirm: true });
|
||||
await tickAt(360);
|
||||
const blocked = await page.evaluate(async mutation => {
|
||||
qaStorageHook = async (_key, value) => {
|
||||
@@ -213,15 +371,28 @@ function paperHTML(total = 25) {
|
||||
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('用户已暂停');
|
||||
if (mutation === 'manualConfirmIncomplete') qaSettings.manualConfirmIncomplete = false;
|
||||
};
|
||||
await qaExam.tick(); return { model: qaModel, state: qaExam.getState() };
|
||||
}, mutation);
|
||||
assert.equal(blocked.model.confirmClicks, 0, '二次确认保存期间发生 ' + mutation + ' 时禁止旧确认点击');
|
||||
assert.equal(blocked.state.enabled, false);
|
||||
}
|
||||
for (const persistedStage of ['submitting', 'confirming']) {
|
||||
await setup({ matched: 21, manualConfirmIncomplete: false, confirm: true });
|
||||
if (persistedStage === 'confirming') await tickAt(360);
|
||||
const blocked = await page.evaluate(async persistedStage => {
|
||||
qaSetClock(360000);
|
||||
qaStorageHook = async (_key, value) => { if (value.stage === persistedStage) qaSettings.manualConfirmIncomplete = true; };
|
||||
await qaExam.tick(); return { state: qaExam.getState(), model: qaModel };
|
||||
}, persistedStage);
|
||||
assert.equal(blocked.model.commitClicks, persistedStage === 'confirming' ? 1 : 0, '保存期间重新开启缺题手动确认,禁止继续原部分题交卷计划');
|
||||
assert.equal(blocked.model.confirmClicks, 0, '保存期间重新开启缺题手动确认,禁止自动确认');
|
||||
assert.equal(blocked.state.enabled, false);
|
||||
}
|
||||
console.log('PASS 最后点击前复核:异步保存期间页面、题库、选中集合、主题、暂停及确认入口变化均阻止旧动作');
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const interruptedRead = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaStorageReads = 0;
|
||||
let release; qaReadGate = new Promise(resolve => { release = resolve; });
|
||||
@@ -237,7 +408,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(interruptedRead.reads, 1);
|
||||
assert.equal(interruptedRead.returned, false);
|
||||
assert.equal(interruptedRead.state.enabled, false, 'storage.get 返回后不得重新启用已暂停的考试');
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
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; } };
|
||||
@@ -248,7 +419,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(interruptedWrite.state.enabled, false);
|
||||
console.log('PASS observe 共享异步读取及 enable 在读写期间被暂停后不复活');
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const retake = await page.evaluate(async () => {
|
||||
qaExam.destroy();
|
||||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||||
@@ -268,7 +439,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(retake.model.commitClicks, 0);
|
||||
assert.equal((await tickAt(360)).model.commitClicks, 1);
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const missingIdentity = await page.evaluate(async () => {
|
||||
qaExam.destroy();
|
||||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||||
@@ -282,7 +453,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(missingIdentity.state.waitSeconds, 360);
|
||||
assert.equal(missingIdentity.model.commitClicks, 0);
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const equalCountdownRetake = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaClock.now = 360000;
|
||||
// Both old checkpoint and new attempt start at 1800. Compare elapsed wall time,
|
||||
@@ -299,7 +470,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(equalCountdownRetake.after.waitSeconds, 360);
|
||||
assert.equal(equalCountdownRetake.model.commitClicks, 0);
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const noClockEvidence = await page.evaluate(async () => {
|
||||
qaExam.destroy(); qaClock.now = 360000;
|
||||
document.querySelector('.pc_examtest_countdown').remove();
|
||||
@@ -315,7 +486,7 @@ function paperHTML(total = 25) {
|
||||
assert.equal(noClockEvidence.after.waitSeconds, 360);
|
||||
assert.equal(noClockEvidence.model.commitClicks, 0);
|
||||
|
||||
await setup({ matched: 21 });
|
||||
await setup();
|
||||
const pendingAttempt = await page.evaluate(async () => {
|
||||
qaExam.destroy();
|
||||
const key = Array.from(qaStorage.keys())[0], saved = qaStorage.get(key);
|
||||
|
||||
Reference in New Issue
Block a user