fix: resume video playback and advance linked exams with attempt controls
This commit is contained in:
+286
-4
@@ -6,6 +6,7 @@ const { chromium } = require('playwright');
|
||||
const ZIP = require('../vendor/jszip.min.js');
|
||||
const workspace = path.resolve(__dirname, '..');
|
||||
const source = fs.readFileSync(path.join(workspace, 'learning-engine.js'), 'utf8');
|
||||
const attemptSource = fs.readFileSync(path.join(workspace, 'attempt-policy.js'), 'utf8');
|
||||
const core = fs.readFileSync(path.join(workspace, 'question-bank-core.js'), 'utf8');
|
||||
const origin = 'http://10.119.67.7/clientcn/';
|
||||
const videoHTML = '<app-course><div class="course-courseware-other-top-left">测试课程</div><li class="cn_courseware_img active" data-courseware-id="v1" data-required-seconds="100"><p>视频一</p><div class="cn_courseware_img_icon">视频</div><div class="menu_courseware_left">已学习20秒</div><div class="courseware_finished_status"><img class="en_courseware_finished en_courseware_noFinished"></div></li><app-course-courseware-video><video id="player1"></video></app-course-courseware-video></app-course>';
|
||||
@@ -20,6 +21,7 @@ async function main() {
|
||||
await page.goto(origin + 'course/test');
|
||||
await page.addScriptTag({ content: core });
|
||||
await page.addScriptTag({ content: source });
|
||||
await page.addScriptTag({ content: attemptSource });
|
||||
let checks = 0;
|
||||
try {
|
||||
async function setup(html = videoHTML, config = {}, settings = {}) {
|
||||
@@ -27,7 +29,9 @@ async function main() {
|
||||
window.qaEngine?.destroy();
|
||||
history.replaceState(null, '', '/clientcn/course/test');
|
||||
document.body.innerHTML = html;
|
||||
window.qaClock = 100000; window.qaMemory = new Map(); window.qaReloads = 0; window.qaNavigations = []; window.qaWorkerURLs = []; window.qaPlayCalls = 0; window.qaPauseCalls = 0; window.qaExamCalls = 0; window.qaQuizCalls = 0;
|
||||
window.qaClock = 100000; window.qaMemory = new Map(); window.qaReloads = 0; window.qaNavigations = []; window.qaWorkerURLs = []; window.qaPlayCalls = 0; window.qaPauseCalls = 0; window.qaExamCalls = 0; window.qaQuizCalls = 0; window.qaEnterCount = 0;
|
||||
window.qaVisibility = 'visible';
|
||||
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => qaVisibility });
|
||||
window.qaVideo = { currentTime: 20, duration: 100, ended: false, paused: true, playbackRate: 1 };
|
||||
window.qaExam = { stage: 'answering', total: 40, verifiedCount: 0 };
|
||||
window.qaSnapshot = { document, url: location.href, accountId: 'fixture-account', bankVersion: 1, planId: 'fixture-plan', courses: ['测试课程'] };
|
||||
@@ -160,7 +164,100 @@ async function main() {
|
||||
state = await page.evaluate(() => qaEngine.start()); assert.equal(state.tasks[0].status, 'waiting');
|
||||
await tick(180000); assert.equal(await page.evaluate(() => qaReloads), 0); checks++;
|
||||
|
||||
// 原站 hidden 分支主动暂停;后台时不进行卡顿刷新,回前台立即续播原位置。
|
||||
await setup(); await page.evaluate(() => qaEngine.start());
|
||||
const playsBeforeHide = await page.evaluate(() => {
|
||||
qaVideo.currentTime = 37.5; qaVisibility = 'hidden'; document.dispatchEvent(new Event('visibilitychange'));
|
||||
document.querySelector('video').pause(); document.querySelector('video').dispatchEvent(new Event('pause'));
|
||||
return qaPlayCalls;
|
||||
});
|
||||
state = await tick(180000); assert.equal(state.video.status, 'backgroundPaused');
|
||||
assert.equal(await page.evaluate(() => qaPlayCalls), playsBeforeHide); assert.equal(await page.evaluate(() => qaReloads), 0);
|
||||
await page.evaluate(() => { qaVisibility = 'visible'; document.dispatchEvent(new Event('visibilitychange')); });
|
||||
await page.waitForFunction(() => !qaVideo.paused);
|
||||
assert.equal(await page.evaluate(() => qaPlayCalls), playsBeforeHide + 1);
|
||||
assert.equal(await page.evaluate(() => qaVideo.currentTime), 37.5); assert.equal((await read()).completed, 0); checks++;
|
||||
|
||||
// 开关关闭、全局暂停、播放器手动暂停均不能被 focus/visible 事件重新播放。
|
||||
await setup(videoHTML, { resumePlaybackOnReturn: false }); await page.evaluate(() => qaEngine.start());
|
||||
await page.evaluate(() => { qaVisibility = 'hidden'; document.dispatchEvent(new Event('visibilitychange')); document.querySelector('video').pause(); });
|
||||
await tick(180000);
|
||||
await page.evaluate(() => { qaVisibility = 'visible'; document.dispatchEvent(new Event('visibilitychange')); });
|
||||
await page.waitForTimeout(50); state = await tick();
|
||||
assert.equal(await page.evaluate(() => qaPlayCalls), 1); assert.equal(state.tasks[0].status, 'waiting');
|
||||
assert.match(state.tasks[0].reason, /自动恢复已关闭/); assert.equal(await page.evaluate(() => qaReloads), 0); checks++;
|
||||
|
||||
for (const kind of ['global', 'local', 'manual', 'quiz']) {
|
||||
await setup(); await page.evaluate(() => qaEngine.start());
|
||||
if (kind === 'global') await page.evaluate(() => qaEngine.pause());
|
||||
if (kind === 'local') await page.evaluate(() => qaEngine.configure({ videoPaused: true }));
|
||||
if (kind === 'manual') {
|
||||
await page.evaluate(() => { qaClock += 1000; const v = document.querySelector('video'); v.style.cssText = 'width:200px;height:100px'; v.onclick = () => { v.pause(); v.dispatchEvent(new Event('pause')); }; });
|
||||
await page.locator('video').click(); await tick();
|
||||
}
|
||||
await page.evaluate(kind => {
|
||||
qaVisibility = 'hidden'; document.dispatchEvent(new Event('visibilitychange')); document.querySelector('video').pause();
|
||||
if (kind === 'quiz') document.querySelector('app-course').insertAdjacentHTML('beforeend', '<div data-learning-quiz><p data-question>等待人工处理的题</p></div>');
|
||||
}, kind);
|
||||
const before = await page.evaluate(() => qaPlayCalls);
|
||||
await page.evaluate(() => { qaVisibility = 'visible'; document.dispatchEvent(new Event('visibilitychange')); });
|
||||
await page.waitForTimeout(50); await tick(180000);
|
||||
assert.equal(await page.evaluate(() => qaPlayCalls), before, kind + ' 优先于自动恢复');
|
||||
assert.equal(await page.evaluate(() => qaReloads), 0); checks++;
|
||||
}
|
||||
|
||||
const playbackPrompt = '<div class="modal-dialog dialog_box"><div class="modal-content"><div class="modal-body modal-dialog-scrollable"><div>是否继续上次播放的位置?</div></div><div class="modal-footer"><button class="btn" type="button" aria-label="Close" onclick="qaRestartClicks++"><span aria-hidden="true">重新开始</span></button><button class="btn" type="button" aria-label="Close" onclick="qaContinueClicks++;qaVideo.currentTime=37.5;this.closest(\'.modal-dialog\').remove()"><span aria-hidden="true">继续播放</span></button></div></div></div>';
|
||||
for (const enabled of [true, false]) {
|
||||
await setup(videoHTML + playbackPrompt, { autoContinuePlayback: enabled });
|
||||
await page.evaluate(() => { window.qaContinueClicks = 0; window.qaRestartClicks = 0; });
|
||||
state = await page.evaluate(() => qaEngine.start());
|
||||
await tick(); await tick(180000);
|
||||
assert.equal(await page.evaluate(() => qaContinueClicks), enabled ? 1 : 0);
|
||||
assert.equal(await page.evaluate(() => qaRestartClicks), 0);
|
||||
if (enabled) assert.equal(await page.evaluate(() => qaVideo.currentTime), 37.5);
|
||||
else { assert.equal(state.tasks[0].status, 'waiting'); assert.equal(await page.evaluate(() => qaPlayCalls), 0); }
|
||||
assert.equal(await page.evaluate(() => qaReloads), 0); checks++;
|
||||
}
|
||||
|
||||
// 唯一弹窗/按钮及全局运行状态是续播前提,不能接管普通“继续播放”按钮或考试弹窗。
|
||||
for (const variant of ['duplicate', 'disabled', 'otherPrompt', 'paused']) {
|
||||
const modal = variant === 'duplicate' ? playbackPrompt + playbackPrompt : variant === 'disabled' ? playbackPrompt.replace('onclick="qaContinueClicks++', 'disabled onclick="qaContinueClicks++') : variant === 'otherPrompt' ? playbackPrompt.replace('是否继续上次播放的位置?', '是否继续考试?') : playbackPrompt;
|
||||
await setup(videoHTML + modal);
|
||||
await page.evaluate(() => { window.qaContinueClicks = 0; window.qaRestartClicks = 0; });
|
||||
if (variant === 'paused') { await page.evaluate(async () => { await qaEngine.start(); await qaEngine.pause(); document.body.insertAdjacentHTML('beforeend', '<button onclick="qaContinueClicks++">继续播放</button>'); }); }
|
||||
else await page.evaluate(() => qaEngine.start());
|
||||
const before = await page.evaluate(() => qaContinueClicks);
|
||||
await tick(180000); assert.equal(await page.evaluate(() => qaContinueClicks), before);
|
||||
if (variant !== 'paused') assert.equal(before, 0);
|
||||
assert.equal(await page.evaluate(() => qaRestartClicks), 0); checks++;
|
||||
}
|
||||
|
||||
const examDetail = '<app-examdetail><div class="examdetail_header">测试课程 已答0/40</div><div class="examdetail_button" onclick="window.qaEnterCount=(window.qaEnterCount||0)+1">开始答题</div></app-examdetail>';
|
||||
for (const enabled of [true, false]) {
|
||||
await setup(examDetail.replace('</app-examdetail>', '<p>允许考试次数:1次</p></app-examdetail>'), { mode: 'exam', skipSingleAttemptQuiz: enabled });
|
||||
state = await page.evaluate(() => qaEngine.start());
|
||||
assert.equal(await page.evaluate(() => window.qaEnterCount || 0), enabled ? 0 : 1);
|
||||
if (enabled) { assert.equal(state.tasks[0].status, 'waiting'); assert.equal(state.exam.requiresManualSubmission, true); }
|
||||
else {
|
||||
await page.evaluate(() => { document.body.innerHTML = '<app-examtest><div class="pc_examtest_title">测试课程</div></app-examtest>'; });
|
||||
await tick(); assert.equal(await page.evaluate(() => qaEngine.getAttemptPolicy().blocked), true, '试卷无次数标签时仍保留同一考试详情证据');
|
||||
}
|
||||
checks++;
|
||||
}
|
||||
await setup(examDetail.replace('</app-examdetail>', '<p>允许考试次数:1次</p></app-examdetail>'), { mode: 'exam', skipSingleAttemptQuiz: false });
|
||||
await page.evaluate(() => { history.replaceState(null, '', '/clientcn/exam/limited'); });
|
||||
await page.evaluate(() => qaEngine.start());
|
||||
await page.evaluate(() => {
|
||||
history.replaceState(null, '', '/clientcn/exam/unrelated/questions');
|
||||
document.body.innerHTML = '<app-examtest><div class="pc_examtest_title">测试课程</div><p>允许考试次数:3次</p></app-examtest>';
|
||||
});
|
||||
state = await tick(); assert.equal(await page.evaluate(() => qaExamCalls), 0, '相同标题的其他考试不得接管');
|
||||
assert.equal(state.tasks.find(t => t.url.includes('/exam/limited')).attemptPolicy.count, 1, '其他考试不得覆写旧考试次数');
|
||||
await page.evaluate(() => {
|
||||
history.replaceState(null, '', '/clientcn/exam/limited/questions');
|
||||
document.body.innerHTML = '<app-examtest><div class="pc_examtest_title">测试课程</div></app-examtest>';
|
||||
});
|
||||
assert.equal(await page.evaluate(() => qaEngine.getAttemptPolicy().count), 1, '同一考试ID的试卷继承详情次数,外部考试不污染'); checks++;
|
||||
await setup(examDetail, { mode: 'exam' });
|
||||
await page.evaluate(() => qaEngine.start()); assert.equal(await page.evaluate(() => qaEnterCount), 1);
|
||||
await page.evaluate(() => { document.body.innerHTML = '<app-examtest><div class="pc_examtest_title">测试课程</div></app-examtest>'; });
|
||||
@@ -198,6 +295,17 @@ async function main() {
|
||||
checks++;
|
||||
}
|
||||
|
||||
await setup(videoHTML.replace('</li>', '<p class="related_test">测试课程</p></li>'), { mode: 'all' });
|
||||
await page.evaluate(() => document.querySelector('.cn_courseware_img').setAttribute('data-is-learned', 'true'));
|
||||
await page.evaluate(() => qaEngine.start());
|
||||
state = await page.evaluate(async html => {
|
||||
qaEngine.destroy(); await new Promise(resolve => setTimeout(resolve, 0));
|
||||
history.replaceState(null, '', '/clientcn/exam/linked-full-navigation'); document.body.innerHTML = html;
|
||||
qaEngine = qaCreate(); return qaEngine.tick();
|
||||
}, examDetail);
|
||||
assert.equal(state.status, 'running', '关联考试完整导航沿用一次性转场授权');
|
||||
assert.equal(state.tasks.filter(t => t.kind === 'exam').length, 1); assert.equal(await page.evaluate(() => qaEnterCount), 1); checks++;
|
||||
|
||||
await setup('<app-examdetail><div class="examdetail_header">没有题库的考试</div><div class="examdetail_button" onclick="window.qaWrongEnter=true">开始答题</div></app-examdetail>', { mode: 'exam' });
|
||||
state = await page.evaluate(() => qaEngine.start()); assert.equal(state.tasks[0].status, 'skipped'); assert.equal(await page.evaluate(() => Boolean(window.qaWrongEnter)), false); checks++;
|
||||
|
||||
@@ -231,10 +339,11 @@ async function main() {
|
||||
qaEngine = qaCreate(); await qaEngine.tick();
|
||||
holdNext = true; const oldTick = qaEngine.tick(); await captured;
|
||||
await qaEngine.configure({ manualConfirmIncomplete: false }); release(); await oldTick;
|
||||
return { state: qaEngine.getState(), shared: await qaStorage.get('control.v1') };
|
||||
const control = await qaStorage.get('control.v1');
|
||||
return { state: qaEngine.getState(), policy: await qaStorage.get('policy:' + control.sessionId + ':manualConfirmIncomplete') };
|
||||
});
|
||||
assert.equal(stalePolicyRead.state.manualConfirmIncomplete, false, '迟到的旧共享读数不得覆盖刚发布的开关');
|
||||
assert.equal(stalePolicyRead.shared.config.manualConfirmIncomplete, false); assert.equal(stalePolicyRead.state.status, 'paused'); checks++;
|
||||
assert.equal(stalePolicyRead.policy.value, false); assert.equal(stalePolicyRead.state.status, 'paused'); checks++;
|
||||
|
||||
// 两个实际标签通过同一GM式存储确认工作页,暂停信号同步,完全隔离真实业务网络。
|
||||
const shared = new Map();
|
||||
@@ -274,7 +383,8 @@ async function main() {
|
||||
assert.equal(state.status, 'paused'); assert.equal(workerState.status, 'paused', '策略同步不能自动恢复任务');
|
||||
assert.equal(await workerPage.evaluate(() => fixtureExamCalls), 1, '修改共享策略期间不得重启答题');
|
||||
const synchronizedControl = shared.get('control.v1');
|
||||
assert.equal(synchronizedControl.status, 'paused'); assert.equal(synchronizedControl.config.manualConfirmIncomplete, true);
|
||||
assert.equal(synchronizedControl.status, 'paused');
|
||||
assert.equal(shared.get('policy:' + synchronizedControl.sessionId + ':manualConfirmIncomplete').value, true);
|
||||
assert.equal(shared.get('session.v1').config.manualConfirmIncomplete, true, '主页面暂停时同步保存工作页策略'); checks++;
|
||||
await page.evaluate(() => qaEngine.resume()); await workerPage.evaluate(() => fixtureEngine.tick()); workerState = await workerPage.evaluate(() => fixtureEngine.tick());
|
||||
assert.equal(workerState.status, 'running'); assert.equal(await workerPage.evaluate(() => fixtureExamCalls), 2, '全局继续后重新核验考试');
|
||||
@@ -341,6 +451,178 @@ async function main() {
|
||||
await page.evaluate(async () => { qaReleaseQuiz(); await qaHeldStart; });
|
||||
assert.equal(await page.evaluate(() => qaQuizCommitted), 0); assert.equal(await page.evaluate(async () => (await qaStorage.get('lease:video')).owner), 'another-worker'); checks++;
|
||||
|
||||
// HTTP 内网页无法依赖 Web Locks:不同标签同时改不同开关,逐键记录不能丢失任一修改。
|
||||
await setup(); await page.evaluate(() => qaEngine.start()); await page.evaluate(() => qaEngine.pause());
|
||||
const concurrentPolicies = await page.evaluate(async () => {
|
||||
qaEngine.destroy();
|
||||
const previous = qaStorage; let gated = false, capturedReads = 0, release, captured;
|
||||
const gate = new Promise(resolve => { release = resolve; });
|
||||
const bothCaptured = new Promise(resolve => { captured = resolve; });
|
||||
qaStorage = {
|
||||
get: async key => {
|
||||
const value = await previous.get(key);
|
||||
if (gated && key === 'control.v1') { if (++capturedReads === 2) captured(); await gate; }
|
||||
return value;
|
||||
}, set: previous.set
|
||||
};
|
||||
qaEngine = qaCreate(); window.qaPolicyPeer = qaCreate();
|
||||
await qaEngine.tick(); await qaPolicyPeer.tick();
|
||||
gated = true;
|
||||
const first = qaEngine.configure({ autoContinuePlayback: false });
|
||||
const second = qaPolicyPeer.configure({ resumePlaybackOnReturn: false });
|
||||
await bothCaptured; release(); await Promise.all([first, second]); gated = false;
|
||||
await qaEngine.tick(); await qaPolicyPeer.tick();
|
||||
const state = qaEngine.getState(), peer = qaPolicyPeer.getState();
|
||||
const saved = await qaStorage.get('session.v1');
|
||||
const result = { state, peer, saved: saved.config,
|
||||
independent: Array.from(qaMemory.keys()).filter(key => key.startsWith('policy:' + saved.id + ':')) };
|
||||
qaPolicyPeer.destroy(); return result;
|
||||
});
|
||||
for (const value of [concurrentPolicies.state, concurrentPolicies.peer, concurrentPolicies.saved]) {
|
||||
assert.equal(value.autoContinuePlayback, false, '并发切换不能丢失第一个标签关闭自动续播');
|
||||
assert.equal(value.resumePlaybackOnReturn, false, '并发切换不能丢失另一个标签关闭切回恢复');
|
||||
}
|
||||
assert.equal(concurrentPolicies.state.status, 'paused'); assert.equal(concurrentPolicies.peer.status, 'paused');
|
||||
assert.equal(concurrentPolicies.independent.length, 2, '不同策略写入两个互不覆盖的独立存储键'); checks++;
|
||||
|
||||
const delayedIndependentRead = await page.evaluate(async () => {
|
||||
const previous = qaStorage; let holdNext = true, release, captured;
|
||||
const gate = new Promise(resolve => { release = resolve; });
|
||||
const held = new Promise(resolve => { captured = resolve; });
|
||||
qaEngine.destroy();
|
||||
qaStorage = {
|
||||
get: async key => {
|
||||
const value = await previous.get(key);
|
||||
if (holdNext && key.endsWith(':autoContinuePlayback')) { holdNext = false; captured(); await gate; }
|
||||
return value;
|
||||
}, set: previous.set
|
||||
};
|
||||
// 初始化先正常读取;随后挂起的旧读取必须晚于新本地修改完成才释放。
|
||||
holdNext = false; qaEngine = qaCreate(); await qaEngine.tick(); holdNext = true;
|
||||
const old = qaEngine.tick(); await held;
|
||||
await qaEngine.configure({ autoContinuePlayback: true }); release(); await old;
|
||||
return qaEngine.getState();
|
||||
});
|
||||
assert.equal(delayedIndependentRead.autoContinuePlayback, true, '迟到的逐键旧读取不能覆盖刚保存的新值');
|
||||
assert.equal(delayedIndependentRead.resumePlaybackOnReturn, false, '更新单键保留另一键的已同步设置');
|
||||
assert.equal(delayedIndependentRead.status, 'paused'); checks++;
|
||||
|
||||
for (const delayedKey of ['control.v1', ':autoContinuePlayback']) {
|
||||
await setup();
|
||||
const pausedDuringRead = await page.evaluate(async delayedKey => {
|
||||
qaEngine.destroy();
|
||||
const previous = qaStorage; let holdNext = false, release, captured;
|
||||
const gate = new Promise(resolve => { release = resolve; });
|
||||
const held = new Promise(resolve => { captured = resolve; });
|
||||
qaStorage = {
|
||||
get: async key => {
|
||||
const value = await previous.get(key);
|
||||
if (holdNext && (delayedKey === 'control.v1' ? key === delayedKey : key.endsWith(delayedKey))) {
|
||||
holdNext = false; captured(); await gate;
|
||||
}
|
||||
return value;
|
||||
}, set: previous.set
|
||||
};
|
||||
qaEngine = qaCreate(); await qaEngine.start();
|
||||
holdNext = true; const oldTick = qaEngine.tick(); await held;
|
||||
await qaEngine.pause(); const paused = qaEngine.getState(); const plays = qaPlayCalls;
|
||||
release(); await oldTick;
|
||||
return { paused, state: qaEngine.getState(), plays, afterPlays: qaPlayCalls, shared: await qaStorage.get('control.v1'), videoPaused: qaVideo.paused };
|
||||
}, delayedKey);
|
||||
assert.equal(pausedDuringRead.paused.status, 'paused');
|
||||
assert.equal(pausedDuringRead.state.status, 'paused', delayedKey + ' 的旧 running 读取返回后不能恢复刚暂停的任务');
|
||||
assert.equal(pausedDuringRead.shared.status, 'paused');
|
||||
assert.equal(pausedDuringRead.afterPlays, pausedDuringRead.plays, '旧异步读取不能额外触发 video.play');
|
||||
assert.equal(pausedDuringRead.videoPaused, true); checks++;
|
||||
}
|
||||
|
||||
// 设置仅发布独立策略,挂起中的旧设置不得覆盖本页或另一执行器随后发布的暂停。
|
||||
for (const pauseElsewhere of [false, true]) {
|
||||
await setup();
|
||||
const pausedDuringConfigure = await page.evaluate(async pauseElsewhere => {
|
||||
qaEngine.destroy();
|
||||
const previous = qaStorage; let holdNext = false, release, captured;
|
||||
const gate = new Promise(resolve => { release = resolve; });
|
||||
const held = new Promise(resolve => { captured = resolve; });
|
||||
qaStorage = {
|
||||
get: async key => {
|
||||
const value = await previous.get(key);
|
||||
if (holdNext && key.endsWith(':autoContinuePlayback')) { holdNext = false; captured(); await gate; }
|
||||
return value;
|
||||
}, set: previous.set
|
||||
};
|
||||
qaEngine = qaCreate(); await qaEngine.start();
|
||||
window.qaPolicyPeer = pauseElsewhere ? qaCreate() : null;
|
||||
if (qaPolicyPeer) await qaPolicyPeer.tick();
|
||||
holdNext = true; const changing = qaEngine.configure({ autoContinuePlayback: false }); await held;
|
||||
const pausing = qaPolicyPeer || qaEngine; await pausing.pause(); const paused = pausing.getState().status; const plays = qaPlayCalls;
|
||||
release(); await changing; await qaEngine.tick();
|
||||
const shared = await qaStorage.get('control.v1');
|
||||
const policy = await qaStorage.get('policy:' + shared.sessionId + ':autoContinuePlayback');
|
||||
const result = { paused, state: qaEngine.getState(), shared, policy, plays, afterPlays: qaPlayCalls, videoPaused: qaVideo.paused };
|
||||
qaPolicyPeer?.destroy(); return result;
|
||||
}, pauseElsewhere);
|
||||
assert.equal(pausedDuringConfigure.paused, 'paused');
|
||||
assert.equal(pausedDuringConfigure.shared.status, 'paused', '旧设置写回不得覆盖' + (pauseElsewhere ? '另一执行器' : '本页') + '的新暂停');
|
||||
assert.equal(pausedDuringConfigure.state.status, 'paused');
|
||||
assert.equal(pausedDuringConfigure.state.autoContinuePlayback, false);
|
||||
assert.equal(pausedDuringConfigure.policy.value, false, '暂停不丢失已请求关闭的独立设置');
|
||||
assert.equal(pausedDuringConfigure.afterPlays, pausedDuringConfigure.plays);
|
||||
assert.equal(pausedDuringConfigure.videoPaused, true); checks++;
|
||||
}
|
||||
|
||||
// 全量 learningSettings 可能是另一标签较早的快照;重载不能重新发布其中的旧策略。
|
||||
await setup(); await page.evaluate(() => qaEngine.start()); await page.evaluate(() => qaEngine.pause());
|
||||
const restoredPolicies = await page.evaluate(async () => {
|
||||
await qaEngine.configure({ autoContinuePlayback: false, resumePlaybackOnReturn: false });
|
||||
const control = await qaStorage.get('control.v1');
|
||||
const keys = ['autoContinuePlayback', 'resumePlaybackOnReturn'].map(key => 'policy:' + control.sessionId + ':' + key);
|
||||
const before = await Promise.all(keys.map(key => qaStorage.get(key)));
|
||||
const plays = qaPlayCalls;
|
||||
qaEngine.destroy(); qaEngine = qaCreate();
|
||||
await qaEngine.configure({ autoContinuePlayback: true, resumePlaybackOnReturn: true, manualConfirmIncomplete: true,
|
||||
skipSingleAttemptQuiz: true, rate: 2, answerInterval: 7, mode: 'video' }, { restore: true });
|
||||
const after = await Promise.all(keys.map(key => qaStorage.get(key)));
|
||||
return { before, after, state: qaEngine.getState(), plays, afterPlays: qaPlayCalls };
|
||||
});
|
||||
assert.equal(restoredPolicies.state.autoContinuePlayback, false, '重载的旧全量快照不能覆盖已发布的自动续播设置');
|
||||
assert.equal(restoredPolicies.state.resumePlaybackOnReturn, false, '重载保留另一标签已发布的切回恢复设置');
|
||||
assert.equal(restoredPolicies.state.status, 'paused');
|
||||
assert.equal(restoredPolicies.state.rate, 2, '普通保存设置仍正常恢复');
|
||||
assert.equal(restoredPolicies.state.answerInterval, 7);
|
||||
assert.equal(restoredPolicies.state.mode, 'video');
|
||||
assert.equal(restoredPolicies.afterPlays, restoredPolicies.plays);
|
||||
assert.deepEqual(restoredPolicies.after, restoredPolicies.before, '恢复不能重新发布已存在的权威策略记录'); checks++;
|
||||
|
||||
// 本地暂停先生效;共享 paused 尚未写完时,新的轮询也不能把旧 running 当作远端继续。
|
||||
await setup();
|
||||
const pendingPauseWrite = await page.evaluate(async () => {
|
||||
qaEngine.destroy();
|
||||
const previous = qaStorage; let holdNext = false, release, captured;
|
||||
const gate = new Promise(resolve => { release = resolve; });
|
||||
const held = new Promise(resolve => { captured = resolve; });
|
||||
qaStorage = {
|
||||
get: previous.get,
|
||||
set: async (key, value) => {
|
||||
if (holdNext && key === 'control.v1' && value.status === 'paused') { holdNext = false; captured(); await gate; }
|
||||
return previous.set(key, value);
|
||||
}
|
||||
};
|
||||
qaEngine = qaCreate(); await qaEngine.start();
|
||||
const plays = qaPlayCalls; holdNext = true; const pausing = qaEngine.pause(); await held;
|
||||
const before = qaEngine.getState(); const oldShared = await qaStorage.get('control.v1');
|
||||
await qaEngine.tick(); const during = qaEngine.getState();
|
||||
release(); await pausing; await qaEngine.tick();
|
||||
return { before, oldShared, during, state: qaEngine.getState(), shared: await qaStorage.get('control.v1'), plays, afterPlays: qaPlayCalls, videoPaused: qaVideo.paused };
|
||||
});
|
||||
assert.equal(pendingPauseWrite.before.status, 'paused');
|
||||
assert.equal(pendingPauseWrite.oldShared.status, 'running', '明确覆盖本地 paused 已生效但共享写入仍挂起的窗口');
|
||||
assert.equal(pendingPauseWrite.during.status, 'paused', '写入挂起期间的新 tick 不能接受旧 running 而恢复任务');
|
||||
assert.equal(pendingPauseWrite.state.status, 'paused');
|
||||
assert.equal(pendingPauseWrite.shared.status, 'paused');
|
||||
assert.equal(pendingPauseWrite.afterPlays, pendingPauseWrite.plays);
|
||||
assert.equal(pendingPauseWrite.videoPaused, true); checks++;
|
||||
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('learning-engine:', checks, 'checks passed (' + (archiveAvailable ? '真实归档DOM' : '合成DOM(未提供原站归档)') + '、平台完成、课中题、恢复、主题预检、租约、并行确认)');
|
||||
} finally { await context.close(); await browser.close(); }
|
||||
|
||||
Reference in New Issue
Block a user