const fs=require('fs'), vm=require('vm'); const src=fs.readFileSync(''+require('path').join(__dirname,'..','public','app.js')+'','utf8'); const a=src.indexOf("const shuffled = a =>"), b=src.indexOf("// ---- People (admin)"); let code=src.slice(a,b); const listeners={}; class FakeAudio{constructor(){this.paused=true;this.currentTime=0;this.duration=100;this.readyState=4;this.error=null;this._l={};this.src='';} addEventListener(e,f){(this._l[e]=this._l[e]||[]).push(f)} fire(e){(this._l[e]||[]).forEach(f=>f())} play(){this.paused=false;this.fire('play');return Promise.resolve()} pause(){if(!this.paused){this.paused=true;this.fire('pause')}} getAttribute(){return this.src||null} load(){}} const toasts=[];const store={d:{},get(k,d){return this.d[k]??d},set(k,v){this.d[k]=v},del(k){delete this.d[k]}}; const el=()=>({remove(){},classList:{toggle(){},add(){}},setAttribute(){},dataset:{},style:{},querySelector:()=>el(),querySelectorAll:()=>[],set innerHTML(v){},set textContent(v){}}); const ctx={Audio:FakeAudio,store,toast:m=>toasts.push(m),px:{art:()=>'x'},icon:()=>'',esc:s=>s,fmtDur:()=>'',$:()=>el(),document:{querySelectorAll:()=>[],querySelector:()=>null}, navigator:{onLine:true},addEventListener:(e,f)=>{listeners[e]=f},setTimeout,clearTimeout,fetch:()=>new Promise(()=>{}),URL:{createObjectURL:()=>'blob:x',revokeObjectURL(){}}, Image:class{},MediaMetadata:class{},openSheet:()=>({}),closeSheet(){},actionSheet(){},Response,URLSearchParams,AbortController,Promise,Set,Map,Date,Math,saveStateHook:0,location:{hash:''}}; vm.createContext(ctx); vm.runInContext(code+";this.pl=pl;this.next=next;this.prev=prev;this.setShuffle=setShuffle;this.setRepeat=setRepeat;this.playQueue=playQueue;this.advance=advance;this.cur=cur;this.recover=recover;this.srcOf=srcOf;",ctx); const T=n=>Array.from({length:n},(_,i)=>({key:'k'+i,title:'t'+i,artist:'a',album:'',thumb:'',src:'/plex/p'+i,dur:1000})); const eq=(n,c)=>{console.log((c?'PASS ':'FAIL ')+n); if(!c)process.exitCode=1}; const sleep=ms=>new Promise(r=>setTimeout(r,ms)); (async()=>{ const {pl}=ctx; ctx.playQueue(T(5),0); eq('plays first',pl.i===0&&pl.a.src==='/plex/p0'); ctx.next(); ctx.next(); ctx.next(); eq('rapid next shows track 3 at once',pl.i===3); await sleep(300); eq('only last loaded',pl.a.src==='/plex/p3'); ctx.setShuffle(true); eq('shuffle keeps current + same tracks',pl.q[pl.i].key==='k3'&&pl.q.length===5&&new Set(pl.q.map(x=>x.key)).size===5&&pl.q.slice(0,4).map(x=>x.key).join()==='k0,k1,k2,k3'); ctx.setShuffle(false); eq('shuffle off restores order',pl.q.map(x=>x.key).join()==='k0,k1,k2,k3,k4'&&pl.i===3); ctx.setRepeat('all'); ctx.advance(true); eq('advance to end',pl.i===4); await sleep(10); ctx.advance(true); eq('repeat all wraps',pl.i===0); ctx.setRepeat('one'); pl.a.currentTime=50; ctx.advance(true); eq('repeat one restarts',pl.i===0&&pl.a.currentTime===0); ctx.setRepeat('off'); pl.i=4; ctx.advance(true); eq('end of queue pauses',pl.a.paused&&pl.i===4); // undecodable file -> transcode fallback, then skip ctx.playQueue(T(3),0); await sleep(10); pl.a.paused=false; pl.a.readyState=0; pl.a.error={code:4}; ctx.recover(true); await sleep(400); eq('falls back to transcode',pl.alt.has('k0')&&pl.a.src.includes('transcode')); pl.a.error={code:4}; ctx.recover(true); await sleep(400); await sleep(300); eq('then skips unplayable track',pl.i===1&&toasts.some(t=>t.includes('Skipping'))); // network stall retries same source pl.a.paused=false;pl.a.readyState=1;pl.a.error=null; const before=pl.retries; ctx.recover(true); await sleep(400); eq('stall reloads stream',pl.retries===before+1); process.exit(process.exitCode||0); })();