fix: start course covers and advance verified learning tasks
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
/* Isolated multi-page navigation and retry regression. All site requests are
|
||||
* fulfilled locally; the archived site's scripts and business APIs never run. */
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'learning-engine.js'), 'utf8');
|
||||
const origin = 'http://10.119.67.7/clientcn/';
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true });
|
||||
const context = await browser.newContext();
|
||||
const blocked = [], errors = [];
|
||||
await context.route('**/*', route => {
|
||||
if (route.request().isNavigationRequest() && route.request().url().startsWith(origin)) return route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><meta charset="utf-8"><body></body>' });
|
||||
blocked.push(route.request().url()); return route.abort();
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(origin + 'learningmap/navigation-fixture');
|
||||
await page.addScriptTag({ content: source });
|
||||
let checks = 0;
|
||||
try {
|
||||
await page.evaluate(async () => {
|
||||
window.qaClock = 100000; window.qaMemory = new Map(); window.qaRequests = []; window.qaPendingNavigation = '';
|
||||
window.qaCourseClicks = [0, 0]; window.qaPlayCalls = 0;
|
||||
window.qaMapURL = location.href;
|
||||
window.qaStorage = { get: async key => qaMemory.has(key) ? structuredClone(qaMemory.get(key)) : null,
|
||||
set: async (key, value) => { qaMemory.set(key, structuredClone(value)); } };
|
||||
window.qaRenderMap = () => {
|
||||
document.body.innerHTML = '<app-learning-map-activity-list>' + ['一', '二'].map((name, index) =>
|
||||
'<div class="activity_content" data-course-id="course-' + index + '"><div class="map_activity_content_info_top"><div class="activity_name">课程' + name + '</div><div class="acitvity_other_info">必修课程</div></div></div>').join('') + '</app-learning-map-activity-list>';
|
||||
document.querySelectorAll('.map_activity_content_info_top').forEach((node, index) => {
|
||||
node.onclick = () => { qaCourseClicks[index]++; history.pushState({}, '', '/clientcn/course/map-course-' + index); qaRenderCourse(index); };
|
||||
});
|
||||
};
|
||||
window.qaRenderCourse = index => {
|
||||
document.body.innerHTML = '<app-course><div data-course-title>课程' + (index ? '二' : '一') + '</div><li class="cn_courseware_img active" data-courseware-id="map-video-' + index + '" data-kind="video"' + (index ? '' : ' data-is-learned="true"') + '><p>视频' + (index ? '二' : '一') + '</p><div class="cn_courseware_img_icon">视频</div><div class="menu_courseware_left">已学习20秒</div></li><app-course-courseware-video><video></video></app-course-courseware-video></app-course>';
|
||||
const media = { currentTime: 20, duration: 100, ended: false, paused: true, playbackRate: 1 };
|
||||
const video = document.querySelector('video');
|
||||
for (const key of Object.keys(media)) Object.defineProperty(video, key, { configurable: true, get: () => media[key], set: value => { media[key] = value; } });
|
||||
video.play = async () => { qaPlayCalls++; media.paused = false; };
|
||||
video.pause = () => { media.paused = true; };
|
||||
};
|
||||
window.qaFlushNavigation = () => {
|
||||
if (!qaPendingNavigation) return false;
|
||||
history.pushState({}, '', qaPendingNavigation); qaPendingNavigation = ''; qaRenderMap(); return true;
|
||||
};
|
||||
qaRenderMap();
|
||||
window.qaEngine = YianLearningEngine.create({ storage: qaStorage, now: () => qaClock,
|
||||
getSnapshot: () => ({ document, url: location.href, planId: 'navigation-fixture', accountId: 'fixture-account' }),
|
||||
navigate: async url => { qaRequests.push(url); qaPendingNavigation = url; } });
|
||||
await qaEngine.configure({ mode: 'video', execution: 'sequential' }); await qaEngine.start();
|
||||
});
|
||||
assert.equal(await page.evaluate(() => qaCourseClicks[0]), 1, '地图原始课程入口被点击一次');
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
const beforeReturn = await page.evaluate(() => ({ state: qaEngine.getState(), url: location.href, requests: qaRequests, pending: qaPendingNavigation }));
|
||||
assert.equal(beforeReturn.requests[0], origin + 'learningmap/navigation-fixture', '下一课程无 href 时返回访问过的原地图地址');
|
||||
assert.equal(beforeReturn.pending, beforeReturn.requests[0]);
|
||||
assert.match(beforeReturn.url, /\/course\/map-course-0$/, '异步导航生效前仍显示旧课程');
|
||||
assert.equal(beforeReturn.state.tasks.find(task => task.title === '视频一').status, 'completed', '课程一的实际平台完成状态被识别');
|
||||
checks++;
|
||||
|
||||
for (let i = 0; i < 3; i++) await page.evaluate(() => qaEngine.tick());
|
||||
const stale = await page.evaluate(() => ({ tasks: qaEngine.getState().tasks, clicks: qaCourseClicks }));
|
||||
const secondCourse = stale.tasks.find(task => task.kind === 'course' && task.title === '课程二');
|
||||
assert(secondCourse, '保留下一课程任务');
|
||||
assert(!['expanded', 'completed'].includes(secondCourse.status), '旧课程仍显示时不能误展开或完成下一课程');
|
||||
assert(!stale.tasks.some(task => task.parentCourse === secondCourse.id), '旧课程课件不能归到下一课程');
|
||||
assert.deepEqual(stale.clicks, [1, 0], '未到地图前不点击错误课程');
|
||||
checks++;
|
||||
|
||||
assert.equal(await page.evaluate(() => qaFlushNavigation()), true);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaEngine.getState().video?.title === '视频二')) break;
|
||||
}
|
||||
const next = await page.evaluate(() => ({ state: qaEngine.getState(), url: location.href, clicks: qaCourseClicks, playCalls: qaPlayCalls }));
|
||||
assert.deepEqual(next.clicks, [1, 1], '回地图后自动点击第二课程一次');
|
||||
assert.equal(next.url, origin + 'course/map-course-1');
|
||||
assert.equal(next.state.video?.title, '视频二', '第二课程的实际视频自动进入调度');
|
||||
assert.equal(next.state.video?.status, 'playing');
|
||||
assert(next.playCalls >= 1);
|
||||
assert.equal(next.state.tasks.filter(task => task.title === '视频一').length, 1, '回地图不会重复添加旧课件');
|
||||
checks++;
|
||||
|
||||
// A timed-out task can be retried explicitly. The retry must click the real
|
||||
// cover span again and receive a fresh loading window, without resetting
|
||||
// platform progress or the bounded automatic-recovery counter.
|
||||
await page.evaluate(async () => {
|
||||
qaEngine.destroy(); await new Promise(resolve => setTimeout(resolve, 0)); qaMemory.clear();
|
||||
history.replaceState({}, '', '/clientcn/course/retry-loading-fixture');
|
||||
document.body.innerHTML = '<app-course><div data-course-title>重试课程</div><div class="course-courseware-playdefult"><span>开始学习</span></div><li class="cn_courseware_img active" data-courseware-id="retry-video" data-kind="video"><p>重试视频</p><div class="cn_courseware_img_icon">视频</div></li></app-course>';
|
||||
window.qaCoverClicks = 0; document.querySelector('.course-courseware-playdefult > span').onclick = () => { qaCoverClicks++; };
|
||||
qaEngine = YianLearningEngine.create({ storage: qaStorage, now: () => qaClock,
|
||||
getSnapshot: () => ({ document, url: location.href, accountId: 'fixture-account' }) });
|
||||
await qaEngine.configure({ mode: 'video' }); await qaEngine.start();
|
||||
});
|
||||
assert.equal(await page.evaluate(() => qaCoverClicks), 1, '首次正常点击实际封面 span');
|
||||
await page.evaluate(async () => { qaClock += 16001; await qaEngine.tick(); });
|
||||
assert.equal(await page.evaluate(() => qaEngine.getState().tasks.find(task => task.kind === 'video').status), 'waiting');
|
||||
const retry = await page.evaluate(async () => { await qaEngine.resume(); return { clicks: qaCoverClicks, state: qaEngine.getState() }; });
|
||||
assert.equal(retry.clicks, 2, '明确继续后再次点击真实封面入口');
|
||||
assert.notEqual(retry.state.tasks.find(task => task.kind === 'video').status, 'waiting', '旧 openedAt 不能让新重试立即超时');
|
||||
await page.evaluate(async () => { qaClock += 14999; await qaEngine.tick(); });
|
||||
assert.notEqual(await page.evaluate(() => qaEngine.getState().tasks.find(task => task.kind === 'video').status), 'waiting', '本次加载有独立的 15 秒等待窗口');
|
||||
await page.evaluate(async () => { qaClock += 1002; await qaEngine.tick(); });
|
||||
assert.equal(await page.evaluate(() => qaEngine.getState().tasks.find(task => task.kind === 'video').status), 'waiting');
|
||||
assert.equal(await page.evaluate(() => qaCoverClicks), 2, '超时期间不循环点击封面');
|
||||
checks++;
|
||||
|
||||
// Display names are not resource identity. While navigation back to the
|
||||
// map is pending, the old course has exactly the same name as the next.
|
||||
await page.evaluate(async () => {
|
||||
qaEngine.destroy(); await new Promise(resolve => setTimeout(resolve, 0)); qaMemory.clear();
|
||||
history.replaceState({}, '', '/clientcn/learningmap/same-title-fixture');
|
||||
window.qaSameMapURL = location.href; window.qaSamePending = ''; window.qaSameClicks = [0, 0];
|
||||
window.qaRenderSameMap = () => {
|
||||
document.body.innerHTML = '<app-learning-map-activity-list>' + ['resource-first', 'resource-second'].map(resource =>
|
||||
'<div class="activity_content" data-course-id="' + resource + '"><div class="map_activity_content_info_top"><div class="activity_name">同名课程</div><div class="acitvity_other_info">必修课程</div></div></div>').join('') + '</app-learning-map-activity-list>';
|
||||
document.querySelectorAll('.map_activity_content_info_top').forEach((node, index) => {
|
||||
node.onclick = () => {
|
||||
qaSameClicks[index]++; history.pushState({}, '', '/clientcn/course/resource?resourceId=' + (index ? 'resource-second' : 'resource-first'));
|
||||
qaRenderCourse(index); document.querySelector('[data-course-title]').textContent = '同名课程';
|
||||
};
|
||||
});
|
||||
};
|
||||
qaRenderSameMap();
|
||||
qaEngine = YianLearningEngine.create({ storage: qaStorage, now: () => qaClock,
|
||||
getSnapshot: () => ({ document, url: location.href, planId: 'same-title-fixture', accountId: 'fixture-account' }),
|
||||
navigate: async url => { qaSamePending = url; } });
|
||||
await qaEngine.configure({ mode: 'video', execution: 'sequential' }); await qaEngine.start(); await qaEngine.tick();
|
||||
});
|
||||
assert.equal(await page.evaluate(() => qaSamePending), origin + 'learningmap/same-title-fixture');
|
||||
for (let i = 0; i < 3; i++) await page.evaluate(() => qaEngine.tick());
|
||||
const sameTitleStale = await page.evaluate(() => ({ state: qaEngine.getState(), url: location.href, clicks: qaSameClicks }));
|
||||
const sameTitleSecond = sameTitleStale.state.tasks.find(task => task.kind === 'course' && task.identity === 'resource-second');
|
||||
assert(sameTitleSecond, '保留相同名称但资源不同的第二课程');
|
||||
assert(!['expanded', 'completed'].includes(sameTitleSecond.status), '同名旧课程不能冒充尚未点击入口的下一资源');
|
||||
assert(!sameTitleStale.state.tasks.some(task => task.parentCourse === sameTitleSecond.id), '同名旧课件不能挂到第二资源');
|
||||
assert.equal(new URL(sameTitleStale.url).searchParams.get('resourceId'), 'resource-first');
|
||||
assert.deepEqual(sameTitleStale.clicks, [1, 0]);
|
||||
await page.evaluate(() => { history.pushState({}, '', qaSamePending); qaSamePending = ''; qaRenderSameMap(); });
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaEngine.getState().video?.title === '视频二')) break;
|
||||
}
|
||||
const sameTitleNext = await page.evaluate(() => ({ state: qaEngine.getState(), url: location.href, clicks: qaSameClicks }));
|
||||
assert.deepEqual(sameTitleNext.clicks, [1, 1]);
|
||||
assert.equal(new URL(sameTitleNext.url).searchParams.get('resourceId'), 'resource-second', '第二资源只能在原地图入口点击后进入');
|
||||
assert.equal(sameTitleNext.state.video?.title, '视频二');
|
||||
assert.equal(sameTitleNext.state.video?.status, 'playing');
|
||||
checks++;
|
||||
|
||||
// A worker reads its task checkpoint after restoring the shared session.
|
||||
// Its old checkpoint must not overwrite the fresh page's loading window.
|
||||
const workerRestore = await page.evaluate(async () => {
|
||||
qaEngine.destroy(); await new Promise(resolve => setTimeout(resolve, 0));
|
||||
const saved = await qaStorage.get('session.v1'); qaMemory.clear();
|
||||
const workerURL = 'http://10.119.67.7/clientcn/course/worker-loading-fixture';
|
||||
const nonce = 'checkpoint-window-fixture'; history.replaceState({}, '', workerURL + '#__yian_worker=' + nonce);
|
||||
document.body.innerHTML = '<app-course><div data-course-title>工作页课程</div><div class="course-courseware-playdefult"><span>开始学习</span></div><li class="cn_courseware_img active" data-courseware-id="worker-video" data-kind="video"><p>工作页视频</p><div class="cn_courseware_img_icon">视频</div></li></app-course>';
|
||||
window.qaWorkerCoverClicks = 0; document.querySelector('.course-courseware-playdefult > span').onclick = () => { qaWorkerCoverClicks++; };
|
||||
const task = YianLearningEngine.discoverTasks(document, workerURL).find(item => item.kind === 'video');
|
||||
Object.assign(task, { status: 'loading', openedAt: qaClock, retryCount: 2, lastRefreshAt: qaClock - 60000 });
|
||||
Object.assign(saved, { status: 'running', tasks: [task], activeTaskId: task.id, accountId: 'fixture-account', planId: '', bankVersion: null, config: Object.assign({}, saved.config, { mode: 'video', execution: 'sequential' }) });
|
||||
await qaStorage.set('session.v1', saved);
|
||||
await qaStorage.set('control.v1', { sessionId: saved.id, status: 'running', config: saved.config });
|
||||
await qaStorage.set('worker:' + nonce, { nonce, sessionId: saved.id, taskId: task.id, url: workerURL, expires: qaClock + 30000, owner: 'old-worker' });
|
||||
await qaStorage.set('task:' + saved.id + ':' + task.id, Object.assign({}, task, { openedAt: qaClock - 90000, coverClickedAt: qaClock - 90000 }));
|
||||
qaEngine = YianLearningEngine.create({ ownerId: 'checkpoint-worker', storage: qaStorage, now: () => qaClock,
|
||||
getSnapshot: () => ({ document, url: location.href, accountId: 'fixture-account' }) });
|
||||
await qaEngine.tick();
|
||||
return { state: qaEngine.getState(), clicks: qaWorkerCoverClicks, worker: await qaStorage.get('worker:' + nonce) };
|
||||
});
|
||||
assert.equal(workerRestore.worker.owner, 'checkpoint-worker', '本例确实恢复受控工作页');
|
||||
assert.equal(workerRestore.state.status, 'running');
|
||||
assert.equal(workerRestore.clicks, 1, 'checkpoint里的旧coverClickedAt不能阻止新工作页点击真实span');
|
||||
assert.notEqual(workerRestore.state.tasks[0].status, 'waiting', '工作页checkpoint读取后仍有新加载窗口');
|
||||
assert.equal(workerRestore.state.tasks[0].retryCount, 2, '页面恢复不清空自动恢复次数');
|
||||
await page.evaluate(async () => { qaClock += 14999; await qaEngine.tick(); });
|
||||
assert.notEqual(await page.evaluate(() => qaEngine.getState().tasks[0].status), 'waiting');
|
||||
await page.evaluate(async () => { qaClock += 1002; await qaEngine.tick(); });
|
||||
assert.equal(await page.evaluate(() => qaEngine.getState().tasks[0].status), 'waiting');
|
||||
assert.equal(await page.evaluate(() => qaWorkerCoverClicks), 1, '工作页新加载窗口内只点一次封面');
|
||||
checks++;
|
||||
|
||||
// Full coordinator chain: video requirements include passing its related
|
||||
// exam, so an ended video is handed off and only the later green website
|
||||
// icon completes the course. Navigation remains asynchronous throughout.
|
||||
await page.evaluate(async () => {
|
||||
qaEngine.destroy(); await new Promise(resolve => setTimeout(resolve, 0)); qaMemory.clear();
|
||||
history.replaceState({}, '', '/clientcn/learningmap/exam-handoff-fixture');
|
||||
window.qaFull = { courseClicks: [0, 0], relatedClicks: 0, enterClicks: 0, coverClicks: 0, firstPlayCalls: 0, examStarts: 0, returns: 0, pending: '', navigations: [], examState: { stage: 'answering', total: 1, verifiedCount: 0 } };
|
||||
window.qaFullURLs = { map: location.href, first: 'http://10.119.67.7/clientcn/course/handoff-first', second: 'http://10.119.67.7/clientcn/course/handoff-second', detail: 'http://10.119.67.7/clientcn/exam/handoff-exam', paper: 'http://10.119.67.7/clientcn/exam/handoff-exam/questions?flag=test' };
|
||||
window.qaFullMap = () => {
|
||||
document.body.innerHTML = '<app-learning-map-activity-list>' + ['联动课程', '后续课程'].map((title, index) => '<div class="activity_content" data-course-id="handoff-course-' + index + '"><div class="map_activity_content_info_top"><div class="activity_name">' + title + '</div><div class="acitvity_other_info">必修课程</div></div></div>').join('') + '</app-learning-map-activity-list>';
|
||||
document.querySelectorAll('.map_activity_content_info_top').forEach((node, index) => { node.onclick = () => {
|
||||
qaFull.courseClicks[index]++; history.pushState({}, '', index ? qaFullURLs.second : qaFullURLs.first); qaFullCourse(index, false);
|
||||
}; });
|
||||
};
|
||||
window.qaFullCourse = (index, returning) => {
|
||||
const title = index ? '后续课程' : '联动课程';
|
||||
document.body.innerHTML = '<app-course><div data-course-title>' + title + '</div><div class="qa-full-player">' + (returning ? '<div class="course-courseware-playdefult"><span>开始学习</span></div>' : '<app-course-courseware-video><video></video></app-course-courseware-video>') + '</div><li class="cn_courseware_img active" data-courseware-id="handoff-video-' + index + '" data-kind="video"><p>' + (index ? '后续视频' : '联动视频') + '</p><div class="cn_courseware_img_icon">视频</div><div class="menu_courseware_left">已学习' + (index ? '20' : '100') + '秒</div><div class="courseware_finished_status"><span class="en_courseware_finished en_courseware_noFinished">尚未完成</span></div>' + (index ? '' : '<p class="related_test">联动课程</p>') + '</li></app-course><div class="completion-tooltip" hidden></div>';
|
||||
const icon = document.querySelector('.en_courseware_noFinished'), tip = document.querySelector('.completion-tooltip');
|
||||
icon.onmouseover = () => { tip.textContent = index ? '1.观看视频进度100%' : '1.观看视频进度100% 2.完成考试答题,并且及格通过'; tip.hidden = false; };
|
||||
icon.onmouseout = () => { tip.hidden = true; };
|
||||
const cover = document.querySelector('.course-courseware-playdefult > span'); if (cover) cover.onclick = () => { qaFull.coverClicks++; };
|
||||
const video = document.querySelector('video');
|
||||
if (video) {
|
||||
const media = { currentTime: index ? 20 : 100, duration: 100, ended: !index, paused: true, playbackRate: 1 };
|
||||
for (const key of Object.keys(media)) Object.defineProperty(video, key, { configurable: true, get: () => media[key], set: value => { media[key] = value; } });
|
||||
video.play = async () => { if (!index) qaFull.firstPlayCalls++; media.paused = false; }; video.pause = () => { media.paused = true; };
|
||||
}
|
||||
const related = document.querySelector('.related_test'); if (related) related.onclick = event => {
|
||||
event.stopPropagation(); qaFull.relatedClicks++; history.pushState({}, '', qaFullURLs.detail);
|
||||
document.body.innerHTML = '<app-examdetail><div class="examdetail_header">联动课程 已答0/1</div><p>总考试次数:3 次</p><div class="examdetail_button">开始答题</div></app-examdetail>';
|
||||
document.querySelector('.examdetail_button').onclick = () => { qaFull.enterClicks++; history.pushState({}, '', qaFullURLs.paper); document.body.innerHTML = '<app-examtest><div class="pc_examtest_title">联动课程</div></app-examtest>'; };
|
||||
};
|
||||
};
|
||||
window.qaFullFlush = () => {
|
||||
if (!qaFull.pending) return false;
|
||||
const url = qaFull.pending; qaFull.pending = ''; history.pushState({}, '', url);
|
||||
if (url === qaFullURLs.map) qaFullMap();
|
||||
else if (url === qaFullURLs.first) { qaFull.returns++; qaFullCourse(0, true); }
|
||||
else throw new Error('Unexpected fixture destination: ' + url);
|
||||
return true;
|
||||
};
|
||||
qaFullMap();
|
||||
qaEngine = YianLearningEngine.create({ storage: qaStorage, now: () => qaClock,
|
||||
getSnapshot: () => ({ document, url: location.href, planId: 'exam-handoff-fixture', accountId: 'fixture-account', courses: ['联动课程'] }),
|
||||
getExamState: () => qaFull.examState, onExamStart: async () => { qaFull.examStarts++; return true; },
|
||||
navigate: async url => { qaFull.navigations.push(url); qaFull.pending = url; } });
|
||||
await qaEngine.configure({ mode: 'all', execution: 'sequential' }); await qaEngine.start();
|
||||
});
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaEngine.getState().tasks.some(task => task.title === '联动视频' && task.status === 'waitingExam'))) break;
|
||||
}
|
||||
const handoff = await page.evaluate(() => ({ state: qaEngine.getState(), flow: qaFull }));
|
||||
assert.equal(handoff.state.tasks.find(task => task.title === '联动视频').status, 'waitingExam', '结束视频按网页提示转考试,不能提前计完成');
|
||||
assert.equal(handoff.state.video?.platformComplete, false);
|
||||
assert.equal(handoff.flow.relatedClicks, 0, '交接阶段保留视频与考试独立状态');
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaFull.examStarts === 1)) break;
|
||||
}
|
||||
const paper = await page.evaluate(() => ({ state: qaEngine.getState(), flow: qaFull, url: location.href }));
|
||||
assert.equal(paper.flow.relatedClicks, 1, '仅点击一次真实关联考试入口');
|
||||
assert.equal(paper.flow.enterClicks, 1, '仅通过正常详情开考按钮进入试卷');
|
||||
assert.equal(paper.flow.examStarts, 1);
|
||||
assert.equal(paper.url, origin + 'exam/handoff-exam/questions?flag=test');
|
||||
assert.equal(paper.state.tasks.filter(task => task.kind === 'exam').length, 1, '目录、详情和试卷为同一场考试');
|
||||
await page.evaluate(() => { qaFull.examState = { stage: 'completed', total: 1, verifiedCount: 1, submitted: true, confirmed: true }; });
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaFull.pending === qaFullURLs.first)) break;
|
||||
}
|
||||
assert.equal(await page.evaluate(() => qaFull.pending), origin + 'course/handoff-first', '考试完成后返回实际原课程核对网站完成标记');
|
||||
for (let i = 0; i < 2; i++) await page.evaluate(() => qaEngine.tick());
|
||||
const stillPaper = await page.evaluate(() => ({ state: qaEngine.getState(), url: location.href, flow: qaFull }));
|
||||
assert.equal(stillPaper.url, origin + 'exam/handoff-exam/questions?flag=test');
|
||||
assert.notEqual(stillPaper.state.tasks.find(task => task.title === '联动视频').status, 'waitingPlatform', '异步返回尚未生效时不能在考试页假装等待课程完成');
|
||||
assert.equal(stillPaper.flow.coverClicks, 0);
|
||||
assert.equal(await page.evaluate(() => qaFullFlush()), true);
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
const returned = await page.evaluate(() => ({ state: qaEngine.getState(), flow: qaFull }));
|
||||
assert.equal(returned.state.tasks.find(task => task.title === '联动视频').status, 'waitingPlatform');
|
||||
assert.equal(returned.flow.returns, 1);
|
||||
assert.equal(returned.flow.coverClicks, 0, '原课程封面出现时不重播已经到末尾的视频');
|
||||
await page.evaluate(() => { const icon = document.querySelector('.en_courseware_noFinished'); icon.classList.remove('en_courseware_noFinished'); icon.textContent = '已完成'; });
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaFull.pending === qaFullURLs.map)) break;
|
||||
}
|
||||
assert.equal(await page.evaluate(() => qaFull.pending), origin + 'learningmap/exam-handoff-fixture');
|
||||
assert.equal(await page.evaluate(() => qaEngine.getState().tasks.find(task => task.title === '联动视频').status), 'completed', '仅真实绿色完成图标才完成原视频任务');
|
||||
assert.equal(await page.evaluate(() => qaFullFlush()), true);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await page.evaluate(() => qaEngine.tick());
|
||||
if (await page.evaluate(() => qaEngine.getState().video?.title === '后续视频')) break;
|
||||
}
|
||||
const full = await page.evaluate(() => ({ state: qaEngine.getState(), flow: qaFull, url: location.href }));
|
||||
assert.deepEqual(full.flow.courseClicks, [1, 1]); assert.equal(full.flow.coverClicks, 0);
|
||||
assert.equal(full.flow.relatedClicks, 1); assert.equal(full.flow.enterClicks, 1); assert.equal(full.flow.examStarts, 1);
|
||||
assert.equal(full.state.tasks.filter(task => task.kind === 'exam').length, 1);
|
||||
assert.equal(full.state.tasks.find(task => task.kind === 'exam').status, 'completed');
|
||||
assert.equal(full.state.tasks.find(task => task.title === '联动视频').status, 'completed');
|
||||
assert.equal(full.url, origin + 'course/handoff-second');
|
||||
assert.equal(full.state.video?.title, '后续视频'); assert.equal(full.state.video?.status, 'playing');
|
||||
checks++;
|
||||
|
||||
assert.deepEqual(blocked, []); assert.deepEqual(errors, []);
|
||||
console.log('course-map-navigation: ' + checks + ' checks passed (map return, resource identity, fresh retry and worker checkpoint windows, video/exam/green-icon/next-course chain; no business requests)');
|
||||
} finally {
|
||||
await page.evaluate(() => window.qaEngine?.destroy()).catch(() => {});
|
||||
await context.close(); await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user