Exercise offline editors and interrupted worker recovery
This commit is contained in:
@@ -15,7 +15,7 @@ async function poll(fn,timeout=60000){const end=Date.now()+timeout;while(Date.no
|
||||
async function run(engine){
|
||||
const cwd=join(scratch,engine);mkdirSync(cwd);cpSync(source,join(cwd,'public'),{recursive:true});
|
||||
const socket=createServer();await new Promise(r=>socket.listen(0,'127.0.0.1',r));const port=socket.address().port;await new Promise(r=>socket.close(r));
|
||||
let origin='http://127.0.0.1:'+port, log='',browser,proxy;
|
||||
let origin='http://127.0.0.1:'+port, log='',browser,proxy,context;
|
||||
const child=spawn('bun',[join(repo,'server/server.js')],{cwd,env:{...process.env,PORT:String(port),ASSET_HASHING:legacy?'0':'1',ASSET_SYNC:legacy?'0':'1',BUILD_TAG:legacy?'1175f1a1d2c1':'',DB_PATH:join(cwd,'db.sqlite'),DATA_DIR:join(cwd,'data'),MEDIA_DIR:join(cwd,'media'),UPLOAD_DIR:join(cwd,'uploads')},stdio:['ignore','pipe','pipe']});
|
||||
for(const stream of [child.stdout,child.stderr])stream.on('data',c=>log=(log+c).slice(-6000));
|
||||
const result={engine,legacy,failures:[],events:[],layouts:[],conditions:[]};results.push(result);
|
||||
@@ -23,8 +23,9 @@ async function run(engine){
|
||||
if(!await poll(async()=>{try{return(await fetch(origin+'/api/version')).ok;}catch{return false;}}))throw Error(log);
|
||||
proxy=createThrottleProxy({targetPort:port,profile:'unthrottled'});origin='http://127.0.0.1:'+await proxy.listen();
|
||||
const manifest=await(await fetch(origin+'/api/manifest')).json();result.build=manifest.buildTag;
|
||||
browser=await({chromium,webkit}[engine]).launch();const context=await browser.newContext();
|
||||
await context.addInitScript(()=>{Object.defineProperty(navigator,'onLine',{configurable:true,get:()=>localStorage.getItem('perf-offline')!=='1'});Object.defineProperty(navigator,'connection',{configurable:true,value:{saveData:localStorage.getItem('perf-save-data')==='1'}});});
|
||||
const type={chromium,webkit}[engine];context=await type.launchPersistentContext(join(cwd,'profile'),{headless:true});browser=context.browser();
|
||||
const configure=async()=>{await context.route('**/*',r=>r.continue());return context.addInitScript(()=>{Object.defineProperty(navigator,'onLine',{configurable:true,get:()=>localStorage.getItem('perf-offline')!=='1'});Object.defineProperty(navigator,'connection',{configurable:true,value:{get saveData(){return localStorage.getItem('perf-save-data')==='1';}}});});};
|
||||
await configure();if(args.includes('--save-data'))await context.addInitScript(()=>localStorage.setItem('perf-save-data','1'));
|
||||
let offline=false;
|
||||
const attach=page=>{
|
||||
page.on('response',r=>{if(offline&&r.status()>=400&&!new URL(r.url()).pathname.startsWith('/api/'))result.failures.push({kind:'http',url:r.url(),status:r.status()});});
|
||||
@@ -41,7 +42,7 @@ async function run(engine){
|
||||
},{manifest,legacy});
|
||||
const started=Date.now();await boot();await page.evaluate(()=>navigator.serviceWorker.ready);await page.evaluate(()=>window.Lazy?.warm());
|
||||
await poll(async()=>!(await inventory()).missing.length,Number(option('--warm-timeout','15000')));
|
||||
result.installMs=Date.now()-started;result.inventory=await inventory();
|
||||
result.installMs=Date.now()-started;result.inventory=await inventory();result.installWire=proxy.getStats();result.saveData=await page.evaluate(()=>navigator.connection?.saveData);
|
||||
if(result.inventory.missing.length)result.failures.push({kind:'incomplete',paths:result.inventory.missing});
|
||||
// Offline navigation always starts from a controlled document.
|
||||
await page.reload();await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('[aria-label="Car mode"]'));
|
||||
@@ -53,7 +54,10 @@ async function run(engine){
|
||||
for(const view of ['search','queue','history','saved','downloads','settings','nowPlaying'])await attempt(view,()=>page.evaluate(async type=>{view={type};if(type==='settings')await renderSettings();else render();await new Promise(r=>setTimeout(r,150));},view));
|
||||
await attempt('sidebar',()=>page.evaluate(()=>{document.querySelector('#menuBtn')?.click();document.querySelector('#sidebar')?.classList.remove('open');}));
|
||||
await attempt('all lazy groups',()=>page.evaluate(async()=>{if(window.Lazy)for(const [name,group] of Object.entries(Lazy.manifest.groups))if(group.background)await Lazy.load(name);}));
|
||||
for(const section of ['piano','midi','floating-lyrics','vocal-reducer','equalizer','setlist-import','broadcast'])await attempt(section,async()=>{await page.evaluate(async()=>{view={type:'settings'};await renderSettings();});await page.waitForSelector('[data-section="'+section+'"]',{timeout:5000});await page.evaluate(id=>document.querySelector('[data-section="'+id+'"]').click(),section);await page.waitForTimeout(100);});
|
||||
for(const section of ['piano','midi','floating-lyrics','vocal-reducer','equalizer','setlist-import','broadcast','party-dj','direct-transfer'])await attempt(section,async()=>{await page.evaluate(async()=>{view={type:'settings'};await renderSettings();});await page.waitForSelector('[data-section="'+section+'"]',{timeout:5000,state:'attached'});await page.evaluate(id=>document.querySelector('[data-section="'+id+'"]').click(),section);await page.waitForTimeout(100);});
|
||||
await attempt('presenter',async()=>{await page.evaluate(async()=>{history.replaceState(null,'','/?present=');await Presenter.boot();});await page.waitForSelector('#presenterView:not(.hidden)',{timeout:5000});await page.locator('#pvCode').fill('x');await page.locator('#pvPairBtn').click();if(!/6-digit/.test(await page.locator('#pvMsg').textContent()))throw Error('Presenter validation did not run');await page.evaluate(()=>{document.querySelector('#presenterView').classList.add('hidden');document.body.classList.remove('presenter-open');history.replaceState(null,'','/');});});
|
||||
await attempt('video editor',async()=>{await page.evaluate(()=>openVideoEditor({id:'upl_0123456789ab',title:'Offline edit',duration:120}));await page.waitForSelector('.video-editor',{timeout:5000});await page.locator('.ve-from').fill('0:10');await page.locator('.ve-to').fill('0:20');await page.locator('.ve-add').click();if(!/10/.test(await page.locator('.ve-cuts').textContent()))throw Error('Cut was not added');await page.evaluate(()=>closeModal());});
|
||||
await attempt('lyrics editor',async()=>{await page.evaluate(async()=>{data.profile={name:'offline-test'};await Notes.load({id:'abcdefghijk',title:'Offline lyrics'});document.querySelector('#lyricsEditBtn').click();});await page.waitForSelector('.lyr-editor-text',{timeout:5000});await page.locator('.lyr-editor-text').fill('[0:10] Offline lyric');await page.evaluate(()=>closeModal());});
|
||||
await attempt('car',async()=>{await page.evaluate(()=>document.querySelector('[aria-label="Car mode"]').click());await page.waitForSelector('.car-mode',{timeout:5000});await page.getByRole('button',{name:'Exit Car mode'}).click();});
|
||||
await attempt('remote',async()=>{await page.evaluate(()=>document.querySelector('#navRemoteBtn').click());await page.waitForSelector('#modal:not(.hidden)',{timeout:5000});await page.evaluate(()=>closeModal());});
|
||||
await attempt('party',async()=>{await page.evaluate(()=>document.querySelector('#navPartyBtn').click());await page.waitForSelector('#partyPanel:not(.hidden)',{timeout:5000});await page.evaluate(()=>document.querySelector('#partyPanel').classList.add('hidden'));});
|
||||
@@ -62,23 +66,36 @@ async function run(engine){
|
||||
await attempt('service',()=>page.evaluate(()=>{enterServiceMode();exitServiceMode();}));
|
||||
await attempt('runtime URLs',async()=>{const failures=await page.evaluate(async manifest=>{const paths=Object.keys(manifest.files).filter(p=>/\.(?:woff2|png|webmanifest)$/.test(p)||/\/(?:opfs-worker|hash-worker|direct-recv-worker|p2p-recv-worker|sha256-wasm)\.js$/.test(p)||p==='/piano-engine.mjs'||p==='/vendor/framework7-swipe.min.js');const bad=[];for(const path of paths){const r=await fetch(path);await r.arrayBuffer();if(!r.ok)bad.push(path+': '+r.status);}return bad;},manifest);if(failures.length)throw Error(failures.join(', '));});
|
||||
}
|
||||
result.staticFailures=result.events.filter(e=>e.kind==='requestfailed'&&new URL(e.url).origin===origin&&!new URL(e.url).pathname.startsWith('/api/'));
|
||||
result.scriptErrors=result.events.filter(e=>e.kind==='pageerror');
|
||||
const apiFailures=result.events.filter(e=>e.kind==='requestfailed'&&new URL(e.url).pathname.startsWith('/api/'));
|
||||
result.expectedTransportErrors=result.events.filter(e=>e.kind==='pageerror'&&engine==='webkit'&&!e.stack&&apiFailures.length&&(e.message==='TypeError: Load failed'||e.message.includes('/api/')));
|
||||
result.staticFailures=result.events.filter(e=>e.kind==='requestfailed'&&new URL(e.url).origin===origin&&!new URL(e.url).pathname.startsWith('/api/')&&new URL(e.url).pathname!=='/sw.js');
|
||||
result.scriptErrors=result.events.filter(e=>e.kind==='pageerror'&&!result.expectedTransportErrors.includes(e));
|
||||
result.failures.push(...result.staticFailures,...result.scriptErrors);
|
||||
// Resumption and Save-Data/two-tab checks use the same persisted CacheStorage.
|
||||
offline=false;if(engine==='chromium')await context.setOffline(false);await proxy.setOffline(false);await page.evaluate(()=>localStorage.removeItem('perf-offline'));
|
||||
if(!legacy&&args.includes('--conditions')){
|
||||
await proxy.setOffline(true);
|
||||
await page.evaluate(async manifest=>{const c=await caches.open('ytplayer-assets');for(const p of Object.keys(manifest.files).filter(p=>manifest.files[p].g!=='core'))await c.delete(p+'?v='+manifest.files[p].h);localStorage.setItem('perf-save-data','1');},manifest);
|
||||
await page.close();page=await context.newPage();attach(page);await boot();
|
||||
const other=await context.newPage();await other.goto(origin);await other.waitForSelector('#carModeBtn');
|
||||
await page.evaluate(()=>navigator.serviceWorker.controller.postMessage({type:'COMPLETE_ASSETS'}));
|
||||
await poll(()=>page.evaluate(async()=>{const c=await caches.open('ytplayer-assets'),s=await(await c.match('/__ytp_asset_state')).json(),r=await c.match('/__ytp_completion/'+s.current.buildTag);return r&&(await r.json()).running;}),10000);
|
||||
const interrupted=await inventory();
|
||||
if(!interrupted.missing.length)throw Error('Interruption did not leave missing assets');
|
||||
if(engine==='chromium'){const cdp=await context.newCDPSession(page);await cdp.send('ServiceWorker.enable');await cdp.send('ServiceWorker.stopAllWorkers');await cdp.detach();}
|
||||
await page.reload();await page.waitForSelector('#carModeBtn');
|
||||
const complete=await poll(async()=>!(await inventory()).missing.length);result.conditions.push({name:'close/reopen, Save-Data, two tabs, worker restart',complete});if(!complete)result.failures.push({kind:'resume',inventory:await inventory()});
|
||||
await page.evaluate(()=>{localStorage.removeItem('perf-save-data');window.dispatchEvent(new Event('online'));});
|
||||
await page.close();await context.close();
|
||||
await proxy.setOffline(false);
|
||||
context=await type.launchPersistentContext(join(cwd,'profile'),{headless:true});browser=context.browser();await configure();
|
||||
page=await context.newPage();attach(page);await boot();
|
||||
const other=await context.newPage();await other.goto(origin);await other.waitForSelector('#carModeBtn');
|
||||
const complete=await poll(async()=>!(await inventory()).missing.length);
|
||||
result.conditions.push({name:'interrupted job, page closed, browser restarted with same profile, Save-Data, two tabs',interruptedMissing:interrupted.missing.length,complete});
|
||||
if(!complete)result.failures.push({kind:'resume',inventory:await inventory()});
|
||||
// Evict one optional file and require the online event alone to restart work.
|
||||
const path='/piano-engine.mjs';await page.evaluate(async({path,manifest})=>{await(await caches.open('ytplayer-assets')).delete(path+'?v='+manifest.files[path].h);localStorage.removeItem('perf-save-data');window.dispatchEvent(new Event('online'));},{path,manifest});
|
||||
const onlineResume=await poll(async()=>!(await inventory()).missing.length);result.conditions.push({name:'online event, Save-Data off',complete:onlineResume});if(!onlineResume)result.failures.push({kind:'online-resume'});
|
||||
await other.close();
|
||||
}
|
||||
console.log(engine,JSON.stringify({missing:result.inventory.missing,failures:result.failures,conditions:result.conditions}));
|
||||
}catch(e){result.failures.push({kind:'harness',error:e.stack});console.log(engine,e.message);}finally{await browser?.close();await proxy?.close();child.kill('SIGTERM');if(child.exitCode===null)await new Promise(r=>child.once('exit',r));}
|
||||
}catch(e){result.failures.push({kind:'harness',error:e.stack});console.log(engine,e.message);}finally{await context?.close();await browser?.close();await proxy?.close();child.kill('SIGTERM');if(child.exitCode===null)await new Promise(r=>child.once('exit',r));}
|
||||
}
|
||||
try{for(const engine of option('--browser','all')==='all'?['chromium','webkit']:[option('--browser')])await run(engine);}finally{writeFileSync(resolve(option('--out',join(repo,'perf/results/offline-complete.json'))),JSON.stringify({results},null,2)+'\n');rmSync(scratch,{recursive:true,force:true});}
|
||||
if(results.some(r=>r.failures.length))process.exitCode=1;
|
||||
|
||||
Reference in New Issue
Block a user