Files
yianxue-assistant/tests/course-entry-workflow.cjs

352 lines
28 KiB
JavaScript

/* Browser plugin not available. Complete bundle and independently modelled site DOM.
* Saved Angular templates supplied the selectors; no archived site scripts run here.
* All business routes are fulfilled locally and no learning record reaches the site. */
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { chromium } = require('playwright');
const workspace = path.resolve(__dirname, '..');
const source = fs.readFileSync(path.join(workspace, '谊安学助手 2.0.user.js'), 'utf8');
const qaDir = path.join(os.tmpdir(), 'yianxue-course-entry-qa');
fs.mkdirSync(qaDir, { recursive: true });
const base = 'http://10.119.67.7/clientcn/';
const courseURL = base + 'course/course-entry-workflow';
const style = `<style>body{margin:24px;background:#eef2f7;color:#21344d;font:15px/1.5 system-ui}main{max-width:760px}h1{font-size:24px}.pc_course_left_top_header{font-size:23px;font-weight:650}.course-courseware-playdefult{height:260px;margin-top:18px;display:grid;place-items:center;background:#dae5ef;border-radius:10px}.course-courseware-playdefult>span{padding:8px 22px;background:#1697ff;color:white;border-radius:24px;cursor:pointer}li{list-style:none;background:white;padding:14px;border-radius:8px;margin-top:12px}li.active{outline:2px solid #649df0}.cn_courseware_img>p{font-weight:600;margin:0 0 8px}.menu_courseware{display:flex;justify-content:space-between}.menu_courseware_left{display:flex;gap:12px;color:#718093}.en_courseware_finished{display:inline-block;width:15px;height:15px;border-radius:50%;background:#29ab6a}.en_courseware_noFinished{background:#f49821}.related_test{color:#2475d3;cursor:pointer}.completion-tooltip{padding:6px;background:#20324b;color:white;position:absolute;z-index:100}video{width:100%;height:260px;background:#172a49}a{color:#2475d3}[hidden]{display:none!important}</style>`;
const cover = '<div class="course-courseware-playdefult"><span>开始学习</span></div>';
function row(index, { complete = false, active = false, exam = false } = {}) {
return '<li class="cn_courseware_img' + (active ? ' active' : '') + '" data-courseware-id="entry-' + index + '"><p><span>' + index + '.</span><span>安全培训视频' + index + '</span></p><div class="menu_courseware"><div class="menu_courseware_left"><div class="cn_courseware_img_icon">视频</div><div>27秒</div></div><div class="menu_courseware_right"><div class="courseware_finished_status"><img class="en_courseware_finished' + (complete ? '' : ' en_courseware_noFinished') + '" alt="' + (complete ? '已完成' : '尚未完成') + '"></div></div></div>' + (exam ? '<div class="related_test_outer"><p class="related_test">入口联动安全课程</p></div>' : '') + '</li>';
}
function documentHTML(body) {
return '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>谊安学课程入口回归</title>' + style + '<body><h1>课程入口与完成状态</h1><main id="site-view">' + body + '</main><div class="completion-tooltip" hidden></div></body></html>';
}
function courseHTML(rows, initialCover = cover) {
return documentHTML('<app-course><div class="pc_course_left_top_header"><span data-course-title>入口联动安全课程</span><div class="pc_course_left_top_header_info">学习时长:10分钟33秒</div></div><div class="qa-player">' + initialCover + '</div><ol>' + rows + '</ol></app-course>');
}
async function setup(browser, html, options = {}) {
const memory = new Map(), requests = [], blocked = [], errors = [];
let holdLoadingWrite = false, releaseWrite = null, writeHeld = false;
const context = await browser.newContext({ viewport: { width: 1366, height: 900 } });
await context.exposeBinding('__gmRead', (_, key, fallback) => memory.has(key) ? structuredClone(memory.get(key)) : fallback);
await context.exposeBinding('__gmWrite', async (_, key, value) => {
if (holdLoadingWrite && /learning:session\.v1$/.test(key) && value?.tasks?.some(task => task.status === 'loading')) {
holdLoadingWrite = false; writeHeld = true;
await new Promise(resolve => { releaseWrite = resolve; });
}
memory.set(key, structuredClone(value));
});
await context.route('**/*', route => {
requests.push(route.request().url());
if (route.request().isNavigationRequest() && route.request().url() === courseURL) return route.fulfill({ status: 200, contentType: 'text/html;charset=utf-8', body: html });
blocked.push(route.request().url()); return route.abort();
});
const page = await context.newPage();
page.on('pageerror', error => errors.push(error.message));
page.on('console', message => { if (message.type() === 'error' || message.type() === 'warning') errors.push(message.text()); });
await page.goto(courseURL);
assert.equal(page.url(), courseURL); assert.equal(await page.title(), '谊安学课程入口回归');
assert(await page.locator('main').innerText(), '页面有真实课程内容');
await page.evaluate(({ delay, persistentCover, requirement }) => {
window.qaEntry = { coverClicks: 0, wrongClicks: 0, rowClicks: [], playCalls: 0, media: null, exams: 0 };
window.qaInstallVideo = () => {
document.querySelector('.qa-player').innerHTML = '<app-course-courseware-video><app-video-base><video id="player1" controls></video></app-video-base></app-course-courseware-video>';
const media = { currentTime: 27, duration: 100, ended: false, paused: true, playbackRate: 1 };
qaEntry.media = media;
const video = document.querySelector('video');
for (const key of ['currentTime', 'duration', 'ended', 'paused', 'playbackRate']) Object.defineProperty(video, key, { configurable: true, get: () => media[key], set: value => { media[key] = value; } });
video.play = async () => { qaEntry.playCalls++; media.paused = false; };
video.pause = () => { media.paused = true; };
};
window.qaWireCover = () => {
document.querySelectorAll('.course-courseware-playdefult > span').forEach(button => button.addEventListener('click', () => {
qaEntry.coverClicks++;
// The site's cover always defaults to the first item, even if already learned.
document.querySelectorAll('.cn_courseware_img').forEach((row, index) => row.classList.toggle('active', index === 0));
if (!persistentCover) setTimeout(qaInstallVideo, delay);
}));
document.querySelectorAll('[data-unknown-start]').forEach(button => button.addEventListener('click', () => { qaEntry.wrongClicks++; }));
};
qaWireCover();
document.querySelectorAll('.cn_courseware_img').forEach((node, index) => {
node.addEventListener('click', event => {
if (event.target.closest('.related_test')) return;
qaEntry.rowClicks.push(index + 1);
document.querySelectorAll('.cn_courseware_img').forEach(row => row.classList.toggle('active', row === node));
// Real li onSelect(item) calls toCourseware directly; it does not show a new cover.
qaEntry.media = null;
document.querySelector('.qa-player').innerHTML = '<p class="qa-loading">正在打开播放器</p>';
if (!persistentCover) setTimeout(qaInstallVideo, delay);
});
node.querySelector('.en_courseware_finished').addEventListener('mouseover', () => {
const tip = document.querySelector('.completion-tooltip');
tip.hidden = false; tip.textContent = requirement || '1.观看视频进度100%' + (node.querySelector('.related_test') ? ' 2.完成考试答题,并且及格通过' : '');
});
node.querySelector('.en_courseware_finished').addEventListener('mouseout', () => { document.querySelector('.completion-tooltip').hidden = true; });
});
document.querySelectorAll('.related_test').forEach(button => button.addEventListener('click', event => {
event.stopPropagation(); qaEntry.exams++;
history.pushState({}, '', '/clientcn/exam/entry-linked');
document.querySelector('#site-view').innerHTML = '<app-examdetail><div class="examdetail_header">入口联动安全课程 已答0/1</div><div>总考试次数:1 次</div><div class="examdetail_button">开始答题</div></app-examdetail>';
document.querySelector('.examdetail_button').onclick = () => { qaEntry.wrongClicks++; };
}));
window.qaComplete = index => {
const node = document.querySelectorAll('.cn_courseware_img')[index - 1];
node.querySelector('.en_courseware_finished').classList.remove('en_courseware_noFinished');
node.querySelector('.en_courseware_finished').alt = '已完成';
};
}, { delay: options.delay == null ? 1100 : options.delay, persistentCover: options.persistentCover === true, requirement: options.requirement || '' });
const sandbox = '(function(){const GM_getValue=(key,fallback)=>window.__gmRead(key,fallback);const GM_setValue=(key,value)=>window.__gmWrite(key,value);\n' + source + '\n})();';
await page.addScriptTag({ content: sandbox }); await page.evaluate(() => YianAssistant.ready);
return { page, context, memory, requests, blocked, errors,
holdLoadingWrite() { holdLoadingWrite = true; },
async waitForLoadingWrite() {
for (let i = 0; i < 100 && !writeHeld; i++) await page.waitForTimeout(50);
assert(writeHeld, '应捕获入口点击前等待中的真实GM存储写入');
},
releaseWrite() { releaseWrite?.(); },
async close() {
assert.deepEqual(blocked, [], '测试不能访问真实业务接口或归档资源');
assert.deepEqual(errors, [], '浏览器无相关错误或警告');
assert.equal(await page.locator('vite-error-overlay,nextjs-portal,#webpack-dev-server-client-overlay').count(), 0);
await context.close();
} };
}
async function configure(page, mode = 'videoQuiz') {
await page.evaluate(async mode => { await YianAssistant.setLearningSetting('mode', mode); await YianAssistant.setLearningSetting('answerInterval', 0); }, mode);
}
async function waitFor(page, predicate, label, timeout = 12000) {
await page.waitForFunction(predicate, null, { timeout }).catch(async error => {
console.error(label, await page.evaluate(() => ({ entry: qaEntry, learning: YianAssistant.getLearningState() }))); throw error;
});
}
(async () => {
const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true });
try {
// The cover receives its click on the child span, exactly as onSelectDefult in the saved template.
const entry = await setup(browser, courseHTML(row(1)));
await configure(entry.page);
await entry.page.waitForTimeout(800);
assert.equal(await entry.page.evaluate(() => qaEntry.coverClicks + qaEntry.rowClicks.length), 0, '未授权开始前不点击封面/课程');
await entry.page.evaluate(() => YianAssistant.taskAction('start'));
await waitFor(entry.page, () => qaEntry.coverClicks === 1 && qaEntry.media && !qaEntry.media.paused, '唯一开始学习入口应被正确点击');
await entry.page.waitForTimeout(2200);
assert.equal(await entry.page.evaluate(() => qaEntry.coverClicks), 1, '播放器异步出现与重复tick不得重复点封面');
assert.deepEqual(await entry.page.evaluate(() => qaEntry.rowClicks), [], '可用的默认封面入口不重复点目录项');
await entry.close();
console.log('PASS 完整bundle:初始无active时唯一真实封面span点击一次,异步播放器出现并正常播放');
const flow = await setup(browser, courseHTML(row(1, { complete: true }) + row(2) + row(3)));
const { page } = flow;
await configure(page);
await page.waitForTimeout(800);
assert.equal(await page.evaluate(() => qaEntry.coverClicks + qaEntry.rowClicks.length), 0, '未授权开始前不点击封面/课程');
await page.evaluate(() => YianAssistant.taskAction('start'));
await waitFor(page, () => qaEntry.rowClicks.includes(2) && qaEntry.media && !qaEntry.media.paused, '首课件完成时应选第二目录项');
assert(!await page.evaluate(() => qaEntry.rowClicks.includes(1)), '平台已完成课件不能重开');
assert.equal(await page.evaluate(() => YianAssistant.getLearningState().completed), 1);
await page.waitForTimeout(2200);
assert.equal(await page.evaluate(() => qaEntry.coverClicks), 0, '默认封面会打开已完成的第一项,必须直接点击第二目录项');
assert.deepEqual(await page.evaluate(() => qaEntry.rowClicks), [2], '播放器异步出现时不得重复点击目录');
const host = page.locator('#yian-question-assistant');
await host.getByRole('button', { name: '展开题库助手', exact: true }).click();
await host.getByTestId('tab-overview').click();
await page.screenshot({ path: path.join(qaDir, '01-cover-started-desktop.png') });
await page.evaluate(() => qaComplete(2));
await waitFor(page, () => qaEntry.rowClicks.includes(3) && qaEntry.media && !qaEntry.media.paused && document.querySelectorAll('.cn_courseware_img')[2].classList.contains('active'), '完成视频二后应自动到视频三');
assert.equal(await page.evaluate(() => qaEntry.rowClicks.filter(index => index === 3).length), 1, '下一课件只点击一次');
assert.equal(await page.evaluate(() => YianAssistant.getLearningState().completed), 2);
await page.evaluate(() => qaComplete(3));
await waitFor(page, () => YianAssistant.getLearningState().status === 'finished' && YianAssistant.getLearningState().completed === 3, '全部课件完成应结束当前课件清单');
assert.equal(await page.evaluate(() => qaEntry.coverClicks), 0);
assert.deepEqual(await page.evaluate(() => qaEntry.rowClicks.filter(index => index === 1)), []);
assert.equal(flow.requests.length, 1, '没有已知下课程入口时不能猜测新网址');
await page.setViewportSize({ width: 390, height: 844 });
await page.screenshot({ path: path.join(qaDir, '02-finished-mobile.png') });
await flow.close();
console.log('PASS 完整bundle:首项平台完成时直接选择未完成目录,播放器异步只开一次→完成后二到三→全部完成跳过;390px/桌面截图与零业务网络');
const paused = await setup(browser, courseHTML(row(1)), { persistentCover: true });
await configure(paused.page);
await paused.page.evaluate(() => YianAssistant.taskAction('start'));
await waitFor(paused.page, () => qaEntry.coverClicks === 1, '封面首次点击');
await paused.page.evaluate(() => YianAssistant.taskAction('pause'));
await paused.page.waitForTimeout(1800);
assert.equal(await paused.page.evaluate(() => qaEntry.coverClicks), 1, '全局暂停不能再点开始学习');
await paused.page.evaluate(() => { qaInstallVideo(); });
await paused.page.waitForTimeout(1400);
assert.equal(await paused.page.evaluate(() => qaEntry.playCalls), 0, '暂停后迟到播放器不应启动');
await paused.page.evaluate(() => YianAssistant.taskAction('resume'));
await waitFor(paused.page, () => qaEntry.playCalls === 1, '明确继续后才播放迟到播放器');
await paused.close();
console.log('PASS 全局暂停使封面流程停止,异步迟到播放器不播放,明确继续后核验再播放');
const partial = await setup(browser, courseHTML(row(1)), { persistentCover: true });
await configure(partial.page);
await partial.page.evaluate(() => YianAssistant.taskAction('start'));
await waitFor(partial.page, () => qaEntry.coverClicks === 1, '局部暂停前封面首次点击');
await partial.page.evaluate(() => YianAssistant.taskAction('videoPause'));
await partial.page.evaluate(() => qaInstallVideo());
await partial.page.waitForTimeout(1700);
assert.equal(await partial.page.evaluate(() => qaEntry.playCalls), 0, '视频局部暂停期间不启动迟到播放器');
assert.equal(await partial.page.evaluate(() => qaEntry.coverClicks), 1);
await partial.close();
console.log('PASS 视频局部暂停也阻止异步播放器启动和封面重试');
const localPending = await setup(browser, courseHTML(row(1)));
await configure(localPending.page); localPending.holdLoadingWrite();
await localPending.page.evaluate(() => { window.qaPendingStart = YianAssistant.taskAction('start'); });
await localPending.waitForLoadingWrite();
await localPending.page.evaluate(() => YianAssistant.taskAction('videoPause'));
localPending.releaseWrite();
await localPending.page.evaluate(() => qaPendingStart);
await localPending.page.waitForTimeout(700);
assert.equal(await localPending.page.evaluate(() => qaEntry.coverClicks + qaEntry.rowClicks.length), 0, '入口存储写入期间局部暂停,迟到的旧动作不得点击封面或目录');
assert.equal(await localPending.page.evaluate(() => YianAssistant.getLearningState().videoPaused), true);
await localPending.page.evaluate(() => YianAssistant.taskAction('videoResume'));
await waitFor(localPending.page, () => qaEntry.coverClicks === 1 && qaEntry.media && !qaEntry.media.paused, '局部继续须重新领取任务后正常点击真实封面');
await localPending.page.waitForTimeout(1200);
assert.equal(await localPending.page.evaluate(() => qaEntry.coverClicks), 1, '局部继续后仅点击封面一次');
assert.deepEqual(await localPending.page.evaluate(() => qaEntry.rowClicks), []);
await localPending.close();
console.log('PASS GM加载写入等待时局部暂停取消旧点击,局部继续重新领取后真实封面只点一次并播放');
for (const [name, badCover] of [['未知按钮', '<button data-unknown-start>开始学习</button>'], ['多个封面按钮', cover + cover]]) {
const bad = await setup(browser, courseHTML(row(1), badCover));
await configure(bad.page); await bad.page.evaluate(() => YianAssistant.taskAction('start'));
await bad.page.waitForTimeout(2200);
assert.equal(await bad.page.evaluate(() => qaEntry.coverClicks + qaEntry.wrongClicks), 0, name + '必须等待人工确认,不能按文案泛点');
await bad.close();
}
console.log('PASS 未知开始学习按钮与多个真实候选都不猜点');
const multiRows = row(1, { exam: true }).replace('</p></div></li>', '</p><p class="related_test">另一份安全考核</p></div></li>');
const multi = await setup(browser, courseHTML(multiRows));
const related = await multi.page.evaluate(() => YianAssistant.getLearningState().tasks.filter(task => task.kind === 'exam'));
assert.deepEqual(related.map(task => task.title), ['入口联动安全课程', '另一份安全考核'], '同一课件两个实际关联考试都需保留,不能在子序号加入前被基础id去重');
assert.equal(new Set(related.map(task => task.id)).size, 2, '每个关联考试入口有独立任务身份');
assert.equal(await multi.page.evaluate(() => qaEntry.exams + qaEntry.coverClicks + qaEntry.rowClicks.length), 0, '只识别多个入口不自动启动');
await multi.close();
console.log('PASS 同一课件多个实际关联考试分别识别,不遗漏、不重复、不擅自开考');
const stale = await setup(browser, courseHTML(row(1)), { persistentCover: true });
await configure(stale.page); stale.holdLoadingWrite();
await stale.page.evaluate(() => { window.qaPendingStart = YianAssistant.taskAction('start'); });
await stale.waitForLoadingWrite();
await stale.page.evaluate(() => {
const old = document.querySelector('app-course');
const replacement = old.cloneNode(true);
replacement.querySelector('.course-courseware-playdefult > span').onclick = () => { qaEntry.wrongClicks++; };
old.replaceWith(replacement);
});
stale.releaseWrite();
await stale.page.evaluate(() => qaPendingStart);
await stale.page.evaluate(() => YianAssistant.taskAction('pause'));
await stale.page.waitForTimeout(700);
assert.equal(await stale.page.evaluate(() => qaEntry.coverClicks + qaEntry.wrongClicks), 0, '同URL同标题的课程根已替换,旧动作不能点击已分离按钮或新封面');
await stale.close();
console.log('PASS 异步存储写入期间同URL同标题的课程根被替换,旧计划不点击迟到封面');
const reordered = await setup(browser, courseHTML(row(1) + row(2)), { persistentCover: true });
await configure(reordered.page); reordered.holdLoadingWrite();
await reordered.page.evaluate(() => { window.qaPendingStart = YianAssistant.taskAction('start'); });
await reordered.waitForLoadingWrite();
await reordered.page.evaluate(() => {
const list = document.querySelector('ol'); list.append(list.firstElementChild);
});
reordered.releaseWrite();
await reordered.page.evaluate(() => qaPendingStart);
await reordered.page.evaluate(() => YianAssistant.taskAction('pause'));
assert.equal(await reordered.page.evaluate(() => qaEntry.coverClicks + qaEntry.rowClicks.length), 0, '同URL同标题同根的目录顺序已变化,旧索引不能点击封面默认项或错行');
await reordered.close();
console.log('PASS 异步加载期间目录重排,仍在文档中的旧按钮也不按过期索引点击');
for (const mode of ['all', 'videoQuiz']) {
const combined = await setup(browser, courseHTML(row(1, { exam: true })));
await configure(combined.page, mode);
await combined.page.evaluate(async () => {
const records = [{ course: '入口联动安全课程', type: '单选', question: '课后核验题', options: [{ label: 'A', text: '准确措施' }, { label: 'B', text: '错误措施' }], answer: 'A' }];
const bytes = new TextEncoder().encode(JSON.stringify(records));
await YianAssistant.importFiles([{ name: '入口联动题库.json', size: bytes.length, arrayBuffer: async () => bytes.buffer }]);
await YianAssistant.taskAction('start');
});
await waitFor(combined.page, () => qaEntry.media && !qaEntry.media.paused, '组合完成条件前视频应正常播放');
assert.equal(await combined.page.evaluate(() => qaEntry.exams), 0, '未达到视频进度要求前不进入考试');
await combined.page.evaluate(() => { qaEntry.media.currentTime = 100; qaEntry.media.ended = true; });
if (mode === 'all') {
await waitFor(combined.page, () => qaEntry.exams === 1 && location.pathname === '/clientcn/exam/entry-linked', '视频达到要求后应转同课件实际关联考试');
await waitFor(combined.page, () => YianAssistant.getLearningState().tasks.some(task => task.kind === 'exam' && task.status === 'waiting'), '单次机会保护应停在真实考试详情');
const state = await combined.page.evaluate(() => YianAssistant.getLearningState());
assert.equal(state.tasks.find(task => task.kind === 'video').status, 'waitingExam', '橙色图标仍需考试,不能把整个课件当作平台已完成');
assert.equal(state.tasks.find(task => task.kind === 'video').progress < 1 || state.video.platformComplete === false, true, '进度满只表示视频阶段,不能伪报平台完成');
assert.notEqual(state.status, 'finished');
assert.equal(await combined.page.evaluate(() => qaEntry.wrongClicks), 0, '仅一次机会交给人工,不点击开始答题');
} else {
await combined.page.waitForTimeout(2400);
assert.equal(await combined.page.evaluate(() => qaEntry.exams), 0, 'videoQuiz模式不能扩大授权自动进入课程考试');
const state = await combined.page.evaluate(() => YianAssistant.getLearningState());
assert.notEqual(state.status, 'finished', '橙图标不能误记完成');
assert.equal(state.video.platformComplete, false);
assert.match(state.tasks.find(task => task.kind === 'video').reason, /考试|模式|完成条件/);
}
await combined.close();
}
console.log('PASS 真实悬浮完成条件:进度满但仍需考试时all让出至唯一关联考试且等待绿图标;videoQuiz保持人工等待,不误报已完成');
const timed = await setup(browser, courseHTML(row(1, { exam: true })), { requirement: '1.观看课件时长达到1分钟 2.完成考试答题' });
await configure(timed.page, 'all');
await timed.page.evaluate(async () => {
const records = [{ course: '入口联动安全课程', type: '单选', question: '时长要求核验', options: [{ label: 'A', text: '准确措施' }, { label: 'B', text: '错误措施' }], answer: 'A' }];
const bytes = new TextEncoder().encode(JSON.stringify(records));
await YianAssistant.importFiles([{ name: '时长要求题库.json', size: bytes.length, arrayBuffer: async () => bytes.buffer }]);
await YianAssistant.taskAction('start');
});
await waitFor(timed.page, () => qaEntry.media && !qaEntry.media.paused, '时长要求下正常启动视频');
await timed.page.evaluate(() => { qaEntry.media.currentTime = 100; qaEntry.media.ended = true; });
await timed.page.waitForTimeout(2200);
assert.equal(await timed.page.evaluate(() => qaEntry.exams), 0, '明确要求平台累计1分钟,视频ended不能取代平台27秒记录');
assert.equal(await timed.page.evaluate(() => YianAssistant.getLearningState().video.platformComplete), false);
await timed.page.evaluate(() => { document.querySelector('.menu_courseware_left > div:last-child').textContent = '60秒'; });
await waitFor(timed.page, () => qaEntry.exams === 1, '平台累计达到明确时长后才进入实际关联考试');
assert.notEqual(await timed.page.evaluate(() => YianAssistant.getLearningState().status), 'finished', '时长达标仍等待考试及绿色整体完成标记');
await timed.close();
console.log('PASS 明确观看时长要求使用平台累计记录;仅ended不越过门槛,累计达到1分钟才转关联考试');
const guarded = await setup(browser, courseHTML(row(1, { exam: true })), { requirement: '1.观看课件时长达到1分钟 2.完成考试答题' });
await configure(guarded.page, 'all');
await guarded.page.evaluate(async () => {
const records = [{ course: '入口联动安全课程', type: '单选', question: '课中核验题', options: [{ label: 'A', text: '准确措施' }, { label: 'B', text: '错误措施' }], answer: 'A' }];
const bytes = new TextEncoder().encode(JSON.stringify(records));
await YianAssistant.importFiles([{ name: '课中保护题库.json', size: bytes.length, arrayBuffer: async () => bytes.buffer }]);
await YianAssistant.taskAction('start');
});
await waitFor(guarded.page, () => qaEntry.media && !qaEntry.media.paused, '保护场景视频先正常播放');
await guarded.page.evaluate(() => {
const popup = document.createElement('div'); popup.className = 'question-modal-pc';
popup.innerHTML = '<p>剩余答题次数:1次</p><div class="question_box"><div class="examtest_content_question"><p class="show"><span>[单选]</span>课中核验题</p></div><div class="examtest_content_list"><div class="examtest_content_list_des"><span class="examtest_content_list_des_optionsort">A</span>准确措施</div><div class="examtest_content_list_des"><span class="examtest_content_list_des_optionsort">B</span>错误措施</div></div></div>';
popup.querySelectorAll('.examtest_content_list_des').forEach(option => { option.onclick = () => { qaEntry.wrongClicks++; }; });
document.body.append(popup);
qaEntry.media.currentTime = 100; qaEntry.media.ended = true;
document.querySelector('.menu_courseware_left > div:last-child').textContent = '60秒';
});
await waitFor(guarded.page, () => YianAssistant.getLearningState().tasks.some(task => task.kind === 'video' && task.status === 'waiting'), '仅一次课中题先交给人工');
await guarded.page.waitForTimeout(1200);
assert.equal(await guarded.page.evaluate(() => qaEntry.exams), 0, '课中弹题存在时即使观看时长达标也不越过进入关联考试');
assert.equal(await guarded.page.evaluate(() => qaEntry.wrongClicks), 0, '一次机会保护无选项点击');
await guarded.page.evaluate(async () => { await YianAssistant.taskAction('pause'); document.querySelector('.question-modal-pc').remove(); });
await guarded.page.waitForTimeout(1200);
assert.equal(await guarded.page.evaluate(() => qaEntry.exams), 0, '手动暂停优先,弹题消失和时长达标也不跳任务');
await guarded.page.evaluate(() => YianAssistant.taskAction('resume'));
await waitFor(guarded.page, () => qaEntry.exams === 1, '人工处理课中题并明确继续后才能转实际关联考试');
await guarded.close();
console.log('PASS 观看达标时课中弹题和暂停优先;一次机会不操作,弹题处理后明确继续才转关联考试');
console.log('截图:' + qaDir);
} finally { await browser.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });