'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}`);} function visibility(h,state){h.context.document.visibilityState=state;const event=new h.context.Event('visibilitychange');h.context.document.dispatchEvent(event);return event;} function site(h){ let timer=true,calls=0; // Mirrors the observed course component: hidden stops, visible restarts. h.context.document.addEventListener('visibilitychange',()=>{calls++;timer=h.context.document.visibilityState!=='hidden';}); return {get timer(){return timer;},get calls(){return calls;},heartbeat(){if(timer&&!h.video.paused){ h.api.receive({id:ID,tail:'sessions/session/heartbeat',body:{resourceId:RID}}, {bizCode:0,data:{resourceCompleted:false,currentPositionSeconds:h.video.currentTime,courseProgressPercent:10}},200); }}}; } (async()=>{ await test('hidden event keeps the website heartbeat alive while real visibility stays hidden',()=>{ const h=harness(),web=site(h);h.api.begin();h.api.track(h.video); const event=visibility(h,'hidden');assert.equal(event.stopped,true);assert.equal(web.timer,true);assert.equal(web.calls,0); assert.equal(h.context.document.visibilityState,'hidden');assert.equal(h.video.playbackRate,2); h.advance(15000);h.video.currentTime+=30;web.heartbeat();assert.equal(h.api.state.active.saved,true); assert.equal(h.api.state.active.serverPosition,150); }); await test('an idle assistant does not intercept website visibility handling',()=>{ const h=harness(),web=site(h);visibility(h,'hidden');assert.equal(web.timer,false);assert.equal(web.calls,1); }); await test('returning to foreground lets the normal website handler run',()=>{ const h=harness(),web=site(h);h.api.begin();visibility(h,'hidden');visibility(h,'visible'); assert.equal(web.calls,1);assert.equal(web.timer,true); }); await test('turning off background mode restores hidden behavior and persists the preference',()=>{ const h=harness(),web=site(h);h.api.begin();visibility(h,'hidden');h.api.setBackgroundEnabled(false); assert.equal(web.timer,false);assert.equal(h.storage.get('icampus.study.v1.background'),'off'); assert.equal(visibility(h,'hidden').stopped,undefined); }); await test('stop while hidden pauses media and restores website cleanup',()=>{ const h=harness(),web=site(h);h.api.begin();h.api.track(h.video);visibility(h,'hidden');h.api.stop(); assert.equal(web.timer,false);assert.equal(h.video.paused,true);assert.equal(h.api.state.running,false); }); await test('pagehide is not suppressed when the tab is closed or reloaded',()=>{ const h=harness();let cleaned=false;h.window.addEventListener('pagehide',()=>{cleaned=true;});h.api.begin();visibility(h,'hidden'); h.window.dispatchEvent(new h.context.Event('pagehide'));assert.equal(cleaned,true);assert.equal(h.api.state.running,false); }); await test('a 60-second background timer delay does not expire the active tab lease',()=>{ const h=harness();h.api.begin();visibility(h,'hidden');h.advance(61000); const lease=JSON.parse(h.storage.get('icampus.study.v1.lease'));assert.ok(lease.until>h.context.Date.now()); }); await test('background delay allows a real heartbeat to arrive before missing saves refresh',()=>{ const h=harness();h.api.begin();h.api.track(h.video);visibility(h,'hidden'); h.advance(80000);h.video.currentTime+=160;h.api.monitor();assert.equal(h.api.state.running,true); h.advance(101000);h.video.currentTime+=202;h.api.monitor();assert.equal(h.api.state.running,false);assert.match(h.api.state.status,/180秒/);assert.equal(h.context.location.reloadCount,1); }); await test('foreground return gives the restarted heartbeat 20 seconds to respond',()=>{ const h=harness();h.api.begin();h.api.track(h.video);visibility(h,'hidden');h.advance(80000);visibility(h,'visible'); h.video.currentTime+=160;h.api.monitor();assert.equal(h.api.state.running,true); h.advance(21000);h.video.currentTime+=42;h.api.monitor();assert.equal(h.api.state.running,false); }); await test('manual pause is respected in background mode',()=>{ const h=harness();h.api.begin();h.api.track(h.video);visibility(h,'hidden');h.video.paused=true;h.api.monitor(); assert.equal(h.api.state.running,false);assert.equal(h.video.playCount,0); }); await test('successful course navigation survives a delayed background tick',async()=>{ const h=harness();h.api.begin();visibility(h,'hidden');h.api.pendingCourse('123');h.context.location.hash='#/my-course/learn?id=123'; h.advance(61000);await h.api.tick();assert.equal(h.api.state.running,true); }); await test('non-course routes do not suppress hidden events',()=>{ const h=harness(),web=site(h);h.api.begin();h.context.location.hash='#/user';visibility(h,'hidden');assert.equal(web.timer,false); }); console.log(`${passed} background playback tests passed.`); })().catch(error=>{console.error(error);process.exitCode=1;});