'use strict'; const assert=require('node:assert/strict'); const {harness,ID,RID}=require('./study-helper.test.cjs'); let passed=0; async function test(name,fn){await fn();passed++;console.log(`PASS ${name}`);} async function start(h){h.api.begin();await h.api.tickCourse(ID);h.advance(1000);await h.api.tickCourse(ID);await Promise.resolve();await Promise.resolve();} function mountPanel(h){ const elements=new Map(); const panel={querySelector(s){if(!elements.has(s))elements.set(s,{textContent:'',value:'all',checked:true});return elements.get(s);}}; h.context.document.body={append(){}}; h.context.document.createElement=()=>({style:{},attachShadow:()=>panel}); h.api.mount();return elements; } (async()=>{ await test('new video gets its own load deadline even after a long course',async()=>{ const h=harness();h.advance(3600000);h.video.readyState=0;h.api.begin();await h.api.tickCourse(ID); assert.equal(h.api.state.running,true);assert.equal(h.video.playCount,0); }); await test('metadata alone does not start playback; loaded data must settle',async()=>{ const h=harness();h.video.readyState=1;h.api.begin();await h.api.tickCourse(ID);assert.equal(h.video.playCount,0); h.video.readyState=2;await h.api.tickCourse(ID);assert.equal(h.video.playCount,0); h.advance(1000);await h.api.tickCourse(ID);assert.equal(h.video.playCount,1);assert.equal(h.video.playbackRate,2); }); await test('stuck loading tries the website play overlay once',async()=>{ const h=harness();let clicks=0;h.selectors.set('.player__play-overlay',{getClientRects:()=>[{}],click(){clicks++;}}); h.video.readyState=0;h.api.begin();await h.api.tickCourse(ID);h.advance(10000);await h.api.tickCourse(ID); h.advance(10000);await h.api.tickCourse(ID);assert.equal(clicks,1);assert.equal(h.api.state.running,true); }); await test('90-second stuck load saves a bounded recovery checkpoint and reloads',async()=>{ const h=harness();h.video.readyState=0;h.api.begin();await h.api.tickCourse(ID);h.advance(90000);await h.api.tickCourse(ID); assert.equal(h.context.location.reloadCount,1);const saved=JSON.parse(h.session.get('icampus.study.v1.recovery')); assert.deepEqual(saved.used,[`${ID}/${RID}`]);assert.equal(saved.hash,h.context.location.hash);assert.equal(h.api.state.running,false); }); await test('a recovered video cannot enter an infinite reload loop',async()=>{ const h=harness();h.api.recovered(RID);h.video.readyState=0;h.api.begin();await h.api.tickCourse(ID);h.advance(90000);await h.api.tickCourse(ID); assert.equal(h.context.location.reloadCount,undefined);assert.equal(h.api.state.running,false);assert.match(h.api.state.status,/恢复一次/); }); await test('source-switch AbortError retries then confirms advancing playback',async()=>{ const h=harness();let calls=0;h.video.paused=true; h.video.play=function(){calls++;if(calls===1)return Promise.reject(Object.assign(new Error('loading'),{name:'AbortError'}));this.paused=false;return Promise.resolve();}; await start(h);assert.equal(h.api.state.running,true);h.advance(2000);h.api.monitor();await Promise.resolve(); h.video.currentTime+=2;h.api.monitor();assert.equal(calls,2);assert.equal(h.api.state.active.phase,'playing'); }); await test('autoplay rejection including muted fallback asks for a click without reloading',async()=>{ const h=harness();h.video.play=()=>Promise.reject(Object.assign(new Error('blocked'),{name:'NotAllowedError'})); await start(h);assert.equal(h.api.state.running,false);assert.match(h.api.state.status,/浏览器阻止/);assert.equal(h.context.location.reloadCount,undefined); }); await test('rebuilt player returns to readiness checking instead of stopping the helper',async()=>{ const h=harness();await start(h);const old=h.video;const replacement={...old,currentSrc:'replacement',readyState:0}; h.selectors.set('video.player__video',replacement);old.isConnected=false;h.api.monitor(); assert.equal(h.api.state.running,true);assert.equal(h.api.state.active,null); await h.api.tickCourse(ID);assert.equal(h.api.state.active,null); replacement.readyState=4;await h.api.tickCourse(ID);h.advance(1000);await h.api.tickCourse(ID); assert.equal(h.api.state.active.video,replacement); }); await test('ordinary pause after confirmed playback remains respected',async()=>{ const h=harness();await start(h);h.video.currentTime+=2;h.api.monitor();h.video.paused=true;h.api.monitor(); assert.equal(h.api.state.running,false);assert.equal(h.video.playCount,1); }); await test('switching resource does not play the previous video source',async()=>{ const h=harness();h.api.details.get(ID).resources[0].completed=true; let secondActive=false;h.buttons[1].classList.contains=()=>secondActive;h.buttons[1].click=()=>{secondActive=true;}; h.api.begin();await h.api.tickCourse(ID);h.advance(5000);await h.api.tickCourse(ID);h.advance(1000);await h.api.tickCourse(ID); assert.equal(h.video.playCount,0); h.video.currentSrc='second-video';await h.api.tickCourse(ID);h.advance(1000);await h.api.tickCourse(ID); assert.equal(h.video.playCount,1);assert.equal(h.api.state.active.id,'2093583972926885888'); }); await test('stop clears pending recovery checkpoint',()=>{ const h=harness();h.session.set('icampus.study.v1.recovery','{}');h.api.stop();assert.equal(h.session.has('icampus.study.v1.recovery'),false); }); await test('recovery reload resumes the same course and consumes its checkpoint',()=>{ const h=harness();h.session.set('icampus.study.v1.recovery',JSON.stringify({hash:h.context.location.hash,expires:200000,mode:'current',autoQuiz:false,used:[`${ID}/${RID}`]})); const ui=mountPanel(h);assert.equal(h.api.state.running,true);assert.equal(ui.get('select').value,'current');assert.equal(ui.get('[data-quiz]').checked,false); assert.equal(h.session.has('icampus.study.v1.recovery'),false); h.api.recoverMedia(ID,RID,'再次失败');assert.equal(h.context.location.reloadCount,undefined);assert.equal(h.api.state.running,false); }); await test('ordinary refresh and mismatched recovery routes remain paused',()=>{ const h=harness();mountPanel(h);assert.equal(h.api.state.running,false); const other=harness();other.session.set('icampus.study.v1.recovery',JSON.stringify({hash:'#/other',expires:200000,used:[]})); mountPanel(other);assert.equal(other.api.state.running,false);assert.equal(other.session.has('icampus.study.v1.recovery'),false); }); console.log(`${passed} playback recovery tests passed.`); })().catch(error=>{console.error(error);process.exitCode=1;});