Files
hub/tools/player-test.js

42 lines
4.3 KiB
JavaScript

const fs=require('fs'), vm=require('vm');
const src=['music.js','player.js','lyrics-view.js'].map(f=>fs.readFileSync(require('path').join(__dirname,'..','public',f),'utf8')).join('\n');
const a=src.indexOf("const shuffled = a =>"), b=src.length;
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={sleepT:{eot:false},sleepClear(){},Audio:FakeAudio,store,toast:m=>toasts.push(m),px:{art:()=>'x'},icon:()=>'',esc:s=>s,fmtDur:()=>'',$:()=>el(),document:{querySelectorAll:()=>[],querySelector:()=>null},allowed:()=>true,
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:'#/tasks'}};
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);
// Plex is told what is playing (shows in its dashboard)
const sent=[]; ctx.fetch=u=>{sent.push(new URL(u,'http://x').searchParams);return Promise.resolve()};
ctx.playQueue(T(3),0); await sleep(300); pl.a.currentTime=12; pl.a.fire('playing');
eq('tells Plex it is playing',sent.length===1&&sent[0].get('state')==='playing'&&sent[0].get('ratingKey')==='k0'&&sent[0].get('time')==='12000');
pl.a.pause(); eq('tells Plex it paused',sent[sent.length-1].get('state')==='paused');
ctx.advance(false); await sleep(300); pl.a.fire('playing');
eq('stops the old song, plays the new',sent.some(p=>p.get('state')==='stopped'&&p.get('ratingKey')==='k0')&&sent[sent.length-1].get('ratingKey')==='k1');
process.exit(process.exitCode||0);
})();