feat: add manual confirmation toggle for incomplete exams
This commit is contained in:
+118
-75
@@ -6,87 +6,130 @@ const path = require('node:path');
|
||||
const { chromium } = require('playwright');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, '谊安学助手 2.0.user.js'), 'utf8');
|
||||
function fixture() {
|
||||
function fixture(partial) {
|
||||
const questions = Array.from({ length: 25 }, (_, i) => `<div app-exam-question><div class="pc_examtest_content"><div class="pc_examtest_content_question"><p>[单选] ${i + 1}. 联动验证题${i}<span class="pc_examtest_content_question_value">(4分)</span></p></div><div class="pc_examtest_content_list"><div class="pc_examtest_content_list_des"><span class="examtest_content_list_des_optionsort">A</span>错误${i}</div><div class="pc_examtest_content_list_des"><span class="examtest_content_list_des_optionsort">B</span>正确${i}</div></div></div></div>`).join('');
|
||||
return `<!doctype html><meta charset="utf-8"><title>助手完整流程验证</title><style>body{font:14px system-ui}.pc_examtest_left{max-width:700px}.pc_examtest_content{padding:8px}.pc_examtest_content_list_des{padding:5px;cursor:pointer}.examtest_content_list_des_checked{background:#ded}.modal[aria-hidden=true]{display:none}</style><app-examtest><div class="pc_examtest"><div class="pc_examtest_left"><div class="pc_examtest_title">完整流程考试</div>${questions}</div><div class="pc_examtest_card">${Array.from({ length: 25 }, (_, i) => `<div class="examtestcard_count_defult">${i + 1}</div>`).join('')}</div><div class="pc_examtest_countdown"><span class="time">00:30:00</span></div><div class="pc_examtest_commit">交卷</div></div></app-examtest><div role="dialog" class="modal" aria-hidden="true"><p>仍有题目未答,是否确认交卷?</p><button>确认交卷</button><button>取消</button></div><div data-exam-result hidden>交卷成功,成绩 84</div>`;
|
||||
return `<!doctype html><meta charset="utf-8"><title>助手完整流程验证</title><style>body{font:14px system-ui}.pc_examtest_left{max-width:700px}.pc_examtest_content{padding:8px}.pc_examtest_content_list_des{padding:5px;cursor:pointer}.examtest_content_list_des_checked{background:#ded}.modal[aria-hidden=true]{display:none}</style><app-examtest><div class="pc_examtest"><div class="pc_examtest_left"><div class="pc_examtest_title">完整流程考试</div>${questions}</div><div class="pc_examtest_card">${Array.from({ length: 25 }, (_, i) => `<div class="examtestcard_count_defult">${i + 1}</div>`).join('')}</div><div class="pc_examtest_countdown"><span class="time">00:30:00</span></div><div class="pc_examtest_commit">交卷</div></div></app-examtest><div role="dialog" class="modal" aria-hidden="true"><p>${partial ? '仍有题目未答,' : ''}是否确认交卷?</p><button>确认交卷</button><button>取消</button></div><div data-exam-result hidden>交卷成功,成绩 ${partial ? 84 : 100}</div>`;
|
||||
}
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true });
|
||||
try {
|
||||
const memory = new Map(), errors = [], blocked = [];
|
||||
const context = await browser.newContext({ viewport: { width: 1366, height: 900 } });
|
||||
await context.exposeBinding('__read', (_, key, fallback) => memory.has(key) ? structuredClone(memory.get(key)) : fallback);
|
||||
await context.exposeBinding('__write', (_, key, value) => memory.set(key, structuredClone(value)));
|
||||
const url = 'http://10.119.67.7/clientcn/exam/workflow/questions?flag=test&totaltime=30&title=' + encodeURIComponent('完整流程考试');
|
||||
await context.route('**/*', route => {
|
||||
if (route.request().isNavigationRequest() && route.request().url() === url) return route.fulfill({ status: 200, contentType: 'text/html;charset=utf-8', body: fixture() });
|
||||
blocked.push(route.request().url()); return route.abort();
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(url);
|
||||
await page.evaluate(() => {
|
||||
window.offset = 0; const nativeNow = Date.now.bind(Date); Date.now = () => nativeNow() + window.offset;
|
||||
window.advanceExamTime = milliseconds => {
|
||||
window.offset += milliseconds;
|
||||
const remaining = Math.max(0, 1800 - Math.floor(window.offset / 1000));
|
||||
document.querySelector('.pc_examtest_countdown .time').textContent = '00:' + String(Math.floor(remaining / 60)).padStart(2, '0') + ':' + String(remaining % 60).padStart(2, '0');
|
||||
};
|
||||
window.commitClicks = 0; window.confirmClicks = 0;
|
||||
document.querySelectorAll('.pc_examtest_content_list_des').forEach(node => node.addEventListener('click', () => {
|
||||
node.parentElement.querySelectorAll('.pc_examtest_content_list_des').forEach(n => n.classList.remove('examtest_content_list_des_checked'));
|
||||
node.classList.add('examtest_content_list_des_checked');
|
||||
}));
|
||||
document.querySelector('.pc_examtest_commit').addEventListener('click', () => {
|
||||
window.commitClicks++; document.querySelector('[role=dialog]').setAttribute('aria-hidden', 'false');
|
||||
for (const { matched, manualConfirmIncomplete = true } of [{ matched: 21 }, { matched: 25 }, { matched: 21, manualConfirmIncomplete: false }]) {
|
||||
const partial = matched < 25;
|
||||
const manual = partial && manualConfirmIncomplete;
|
||||
const memory = new Map(), errors = [], blocked = [];
|
||||
const context = await browser.newContext({ viewport: { width: 1366, height: 900 } });
|
||||
await context.exposeBinding('__read', (_, key, fallback) => memory.has(key) ? structuredClone(memory.get(key)) : fallback);
|
||||
await context.exposeBinding('__write', (_, key, value) => memory.set(key, structuredClone(value)));
|
||||
const url = 'http://10.119.67.7/clientcn/exam/workflow/questions?flag=test&totaltime=30&attemptId=workflow-' + matched + '-' + manualConfirmIncomplete + '&title=' + encodeURIComponent('完整流程考试');
|
||||
await context.route('**/*', route => {
|
||||
if (route.request().isNavigationRequest() && route.request().url() === url) return route.fulfill({ status: 200, contentType: 'text/html;charset=utf-8', body: fixture(partial) });
|
||||
blocked.push(route.request().url()); return route.abort();
|
||||
});
|
||||
document.querySelector('[role=dialog] button').addEventListener('click', () => {
|
||||
window.confirmClicks++; document.querySelector('[role=dialog]').setAttribute('aria-hidden', 'true'); document.querySelector('[data-exam-result]').hidden = false;
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(url);
|
||||
async function mountPage() {
|
||||
await page.evaluate(() => {
|
||||
window.offset = 0; const nativeNow = Date.now.bind(Date); Date.now = () => nativeNow() + window.offset;
|
||||
window.advanceExamTime = milliseconds => {
|
||||
window.offset += milliseconds;
|
||||
const remaining = Math.max(0, 1800 - Math.floor(window.offset / 1000));
|
||||
document.querySelector('.pc_examtest_countdown .time').textContent = '00:' + String(Math.floor(remaining / 60)).padStart(2, '0') + ':' + String(remaining % 60).padStart(2, '0');
|
||||
};
|
||||
window.commitClicks = 0; window.confirmClicks = 0;
|
||||
document.querySelectorAll('.pc_examtest_content_list_des').forEach(node => node.addEventListener('click', () => {
|
||||
node.parentElement.querySelectorAll('.pc_examtest_content_list_des').forEach(n => n.classList.remove('examtest_content_list_des_checked'));
|
||||
node.classList.add('examtest_content_list_des_checked');
|
||||
}));
|
||||
document.querySelector('.pc_examtest_commit').addEventListener('click', () => {
|
||||
window.commitClicks++; document.querySelector('[role=dialog]').setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
document.querySelector('[role=dialog] button').addEventListener('click', () => {
|
||||
window.confirmClicks++; document.querySelector('[role=dialog]').setAttribute('aria-hidden', 'true'); document.querySelector('[data-exam-result]').hidden = false;
|
||||
});
|
||||
});
|
||||
await page.addScriptTag({ content: '(function(){const GM_getValue=(k,v)=>window.__read(k,v);const GM_setValue=(k,v)=>window.__write(k,v);\n' + script + '\n})();' });
|
||||
await page.evaluate(() => YianAssistant.ready);
|
||||
}
|
||||
await mountPage();
|
||||
await page.evaluate(() => { YianAssistant.getState(); });
|
||||
await page.getByTestId('tab-exam').click();
|
||||
const manualSwitch = page.getByRole('switch', { name: '题目不全时手动确认', exact: true });
|
||||
assert.equal(await manualSwitch.isChecked(), true, '新设置默认开启缺题手动确认');
|
||||
if (!manualConfirmIncomplete) {
|
||||
await manualSwitch.click();
|
||||
await page.waitForFunction(() => YianAssistant.getLearningState().manualConfirmIncomplete === false);
|
||||
await page.waitForFunction(async () => (await window.__read('yianxue.assistant.setting.v2.learningSettings', null))?.manualConfirmIncomplete === false);
|
||||
assert.equal(await manualSwitch.isChecked(), false);
|
||||
await page.reload(); await mountPage();
|
||||
await page.getByTestId('tab-exam').click();
|
||||
assert.equal(await manualSwitch.isChecked(), false, '刷新后记住已明确关闭的缺题确认开关');
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getLearningState().manualConfirmIncomplete), false);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getState().autoAnswer), false, '设置恢复不会自行开始答题');
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().enabled), false, '刷新仍需明确开始交卷计划');
|
||||
}
|
||||
const records = Array.from({ length: matched }, (_, i) => ({ role: i % 2 ? '主操' : '班长', course: '流程主题库', type: '单选', number: i + 1,
|
||||
question: '联动验证题' + i, options: [{ label: 'A', text: '正确' + i }, { label: 'B', text: '错误' + i }], answer: 'A' }));
|
||||
await page.evaluate(async records => {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(records));
|
||||
await YianAssistant.importFiles([{ name: '统一流程.json', size: bytes.length, arrayBuffer: async () => bytes.buffer }]);
|
||||
await YianAssistant.setLearningSetting('mode', 'exam');
|
||||
await YianAssistant.setLearningSetting('answerInterval', 0);
|
||||
}, records);
|
||||
await page.waitForFunction(() => YianAssistant.getState().pageReady);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.setAutoAnswer(true)), false, '没有明确主题关联不能答题');
|
||||
await page.evaluate(async () => {
|
||||
await YianAssistant.setLearningSetting('associations', { 完整流程考试: ['流程主题库'] });
|
||||
await YianAssistant.taskAction('start');
|
||||
});
|
||||
});
|
||||
await page.addScriptTag({ content: '(function(){const GM_getValue=(k,v)=>window.__read(k,v);const GM_setValue=(k,v)=>window.__write(k,v);\n' + script + '\n})();' });
|
||||
await page.evaluate(() => YianAssistant.ready);
|
||||
await page.evaluate(() => { YianAssistant.getState(); });
|
||||
const records = Array.from({ length: 21 }, (_, i) => ({ role: i % 2 ? '主操' : '班长', course: '流程主题库', type: '单选', number: i + 1,
|
||||
question: '联动验证题' + i, options: [{ label: 'A', text: '正确' + i }, { label: 'B', text: '错误' + i }], answer: 'A' }));
|
||||
await page.evaluate(async records => {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(records));
|
||||
await YianAssistant.importFiles([{ name: '统一流程.json', size: bytes.length, arrayBuffer: async () => bytes.buffer }]);
|
||||
await YianAssistant.setLearningSetting('mode', 'exam');
|
||||
await YianAssistant.setLearningSetting('answerInterval', 0);
|
||||
}, records);
|
||||
await page.waitForFunction(() => YianAssistant.getState().pageReady);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.setAutoAnswer(true)), false, '没有明确主题关联不能答题');
|
||||
await page.evaluate(async () => {
|
||||
await YianAssistant.setLearningSetting('associations', { 完整流程考试: ['流程主题库'] });
|
||||
await YianAssistant.taskAction('start');
|
||||
});
|
||||
await page.waitForFunction(() => YianAssistant.getState().metrics.selected === 21, null, { timeout: 15000 });
|
||||
await page.waitForFunction(() => YianAssistant.getExamState().predictedScore === 84);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 0);
|
||||
await page.getByTestId('tab-bank').click();
|
||||
await page.getByRole('textbox', { name: '搜索题目', exact: true }).fill('联动验证题0');
|
||||
await page.getByRole('button', { name: '搜索', exact: true }).click();
|
||||
assert.equal(await page.getByRole('switch', { name: '导出题库', exact: true, includeHidden: true }).isChecked(), false);
|
||||
await page.getByTestId('tab-console').click();
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().enabled), true, '切栏不取消交卷授权');
|
||||
await page.evaluate(() => window.advanceExamTime(4 * 60000));
|
||||
await page.waitForTimeout(750);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 0, '4分钟不得交卷');
|
||||
await page.evaluate(() => window.advanceExamTime(2 * 60000 + 500));
|
||||
await page.waitForFunction(() => YianAssistant.getExamState().submitted === true, null, { timeout: 8000 }).catch(async error => {
|
||||
console.error(await page.evaluate(() => ({ stage: YianAssistant.getExamState().stage, message: YianAssistant.getExamState().message,
|
||||
score: YianAssistant.getExamState().predictedScore, tasks: YianAssistant.getLearningState().tasks,
|
||||
commits: window.commitClicks, confirms: window.confirmClicks, ready: YianAssistant.getContext().ready })));
|
||||
throw error;
|
||||
});
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 1);
|
||||
assert.equal(await page.evaluate(() => window.confirmClicks), 1);
|
||||
await page.waitForFunction(() => YianAssistant.getLearningState().completed === 1);
|
||||
await page.waitForTimeout(1300);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 1);
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(blocked, []);
|
||||
console.log('PASS 完整用户脚本:无主题阻止答题→明确关联→统一题库21题全文回读=84分→切栏不扰动→4分钟等待→6分钟正常交卷→网站确认→任务完成,零业务请求');
|
||||
await page.waitForFunction(matched => YianAssistant.getState().metrics.selected === matched, matched, { timeout: 15000 });
|
||||
await page.waitForFunction(matched => YianAssistant.getExamState().predictedScore === matched * 4, matched);
|
||||
if (manual) {
|
||||
await page.waitForFunction(() => YianAssistant.getExamState().stage === 'waiting' && YianAssistant.getExamState().requiresManualSubmission === true);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().unverifiedCount), 4);
|
||||
assert.match(await page.evaluate(() => YianAssistant.getExamState().message), /人工|手动/);
|
||||
}
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().requiresManualSubmission), manual);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 0);
|
||||
await page.getByTestId('tab-bank').click();
|
||||
await page.getByRole('textbox', { name: '搜索题目', exact: true }).fill('联动验证题0');
|
||||
await page.getByRole('button', { name: '搜索', exact: true }).click();
|
||||
assert.equal(await page.getByRole('switch', { name: '导出题库', exact: true, includeHidden: true }).isChecked(), false);
|
||||
await page.getByTestId('tab-console').click();
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().enabled), !manual, '切栏保留开关决定的交卷策略和当前授权状态');
|
||||
await page.evaluate(() => window.advanceExamTime(4 * 60000));
|
||||
await page.waitForTimeout(750);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 0, '4分钟不得交卷');
|
||||
await page.evaluate(() => window.advanceExamTime(2 * 60000 + 500));
|
||||
if (manual) {
|
||||
await page.waitForTimeout(1300);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 0, '84 分且满 6 分钟,缺题仍不得自动点击交卷');
|
||||
assert.equal(await page.evaluate(() => window.confirmClicks), 0);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().requiresManualSubmission), true);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().stage), 'waiting');
|
||||
await page.locator('.pc_examtest_commit').click();
|
||||
await page.getByRole('button', { name: '确认交卷', exact: true }).click();
|
||||
}
|
||||
await page.waitForFunction(() => YianAssistant.getExamState().submitted === true, null, { timeout: 8000 }).catch(async error => {
|
||||
console.error(await page.evaluate(() => ({ stage: YianAssistant.getExamState().stage, message: YianAssistant.getExamState().message,
|
||||
score: YianAssistant.getExamState().predictedScore, tasks: YianAssistant.getLearningState().tasks,
|
||||
commits: window.commitClicks, confirms: window.confirmClicks, ready: YianAssistant.getContext().ready })));
|
||||
throw error;
|
||||
});
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 1);
|
||||
assert.equal(await page.evaluate(() => window.confirmClicks), 1);
|
||||
assert.equal(await page.evaluate(() => YianAssistant.getExamState().requiresManualSubmission), false, '网站确认结果后清除待人工交卷提示');
|
||||
await page.waitForFunction(() => YianAssistant.getLearningState().completed === 1);
|
||||
await page.waitForTimeout(1300);
|
||||
assert.equal(await page.evaluate(() => window.commitClicks), 1);
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(blocked, []);
|
||||
console.log(manual
|
||||
? 'PASS 完整用户脚本:21/25 题全文回读=84分→切栏不扰动→满6分钟仍无自动交卷→原网页真实人工交卷和确认→网站结果→任务完成,零业务请求'
|
||||
: partial
|
||||
? 'PASS 完整用户脚本:真实界面关闭缺题确认开关→21/25题=84分→4分钟等待→满6分钟自动交卷并确认未答提醒→网站结果→任务完成,零业务请求'
|
||||
: 'PASS 完整用户脚本:25/25题全文回读=100分→切栏不扰动→4分钟等待→满6分钟自动交卷和确认→网站结果→任务完成,零业务请求');
|
||||
await context.close();
|
||||
}
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
|
||||
Reference in New Issue
Block a user