Files

136 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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) => `<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>${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 {
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; });