/* Full userscript workflow, simulated business URL only: no business requests. */ 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); 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(partial) { const questions = Array.from({ length: 25 }, (_, i) => `

[单选] ${i + 1}. 联动验证题${i}(4分)

A错误${i}
B正确${i}
`).join(''); return `助手完整流程验证
完整流程考试
${questions}
${Array.from({ length: 25 }, (_, i) => `
${i + 1}
`).join('')}
00:30:00
交卷
`; } (async () => { const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true }); try { 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(); }); 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.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; });