#!/usr/bin/env node // Phase 3 checks against the real Bun server, a same-origin restart and real workers. import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { mkdtempSync, cpSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { createServer } from 'node:net'; import { chromium, webkit } from 'playwright'; import { createThrottleProxy } from './proxy.mjs'; const repo=resolve(new URL('..',import.meta.url).pathname), scratch=mkdtempSync(join(repo,'perf/.tmp/lazy-')); const sleep=ms=>new Promise(r=>setTimeout(r,ms)); async function wait(page,fn,arg,timeout=90000){const end=Date.now()+timeout;while(Date.now(){ await Lazy.load('layout:'+Lazy.layout); const settle=()=>new Promise(resolve=>setTimeout(resolve,300));await settle(); const nodes=[...document.querySelectorAll('body,body *')],props=['display','position','color','background-color','font-family','font-size','padding','margin','border-radius','--bg','--text','--radius']; const snapshot=()=>nodes.map(n=>props.map(p=>getComputedStyle(n).getPropertyValue(p))); const links=[...document.querySelectorAll('link[data-lazy-css]')],positions=links.map(n=>[n,n.nextSibling]),before=snapshot(); const loaded=()=>new Promise((resolve,reject)=>{let tries=0;function poll(){if(links.every(n=>n.sheet))return resolve();if(tries++>120)return reject(Error('Reordered CSS did not load'));requestAnimationFrame(poll);}poll();}); links.forEach(n=>document.head.append(n));await loaded();await settle();const after=snapshot(); for(const [n,next] of positions.reverse())document.head.insertBefore(n,next); await loaded();await settle(); return before.flatMap((values,i)=>values.flatMap((v,j)=>v===after[i][j]?[]:[{element:nodes[i].id||nodes[i].className,property:props[j],before:v,after:after[i][j]}])); }); assert.deepEqual(changes,[],'requesting scoped layout CSS earlier preserves its computed appearance'); } const args=process.argv.slice(2), requested=args.includes('--browser')?args[args.indexOf('--browser')+1]:'all'; const results=[]; async function run(name){ const cwd=join(scratch,name);mkdirSync(cwd);cpSync(join(repo,'frontend'),join(cwd,'public'),{recursive:true}); const portServer=createServer();await new Promise(r=>portServer.listen(0,'127.0.0.1',r));const port=portServer.address().port;await new Promise(r=>portServer.close(r)); let child,browser,proxy,log=''; async function stop(){if(!child)return;const p=child;child=null;if(p.exitCode===null){p.kill('SIGTERM');await new Promise(r=>p.once('exit',r));}} async function start(){log='';child=spawn('bun',[join(repo,'server/server.js')],{cwd,env:{...process.env,PORT:String(port),ASSET_HASHING:'1',ASSET_SYNC:'1',BUILD_TAG:'',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(-15000));for(let i=0;i<200;i++){if(child.exitCode!==null)throw Error(log);try{if((await fetch('http://127.0.0.1:'+port+'/api/version')).ok)return;}catch{}await sleep(100);}throw Error(log);} try { await start();proxy=createThrottleProxy({targetPort:port,profile:'unthrottled'});const origin='http://127.0.0.1:'+await proxy.listen();proxy.startTracking(); browser=await ({chromium,webkit}[name]).launch({headless:true});const context=await browser.newContext();let disconnected=false; await context.route('**/*',r=>r.request().url().startsWith(origin)?r.continue():r.abort()); await context.addInitScript(()=>{window.__saveData=localStorage.getItem('perf-save-data')==='1';const observer=new MutationObserver(()=>{if(document.body){window.__firstBodyLayout=document.documentElement.dataset.layout;observer.disconnect();}});observer.observe(document,{childList:true,subtree:true});Object.defineProperty(navigator,'onLine',{configurable:true,get:()=>localStorage.getItem('perf-offline')!=='1'});Object.defineProperty(navigator,'connection',{configurable:true,value:{get saveData(){return window.__saveData;}}});}); const page=await context.newPage(),errors=[],failedRequests=[];page.on('requestfailed',request=>{if(disconnected)failedRequests.push(request.url());});page.on('pageerror',e=>errors.push({message:e.message,disconnected}));page.on('console',m=>{if(m.type()==='error')console.log(name,'console:',m.text());}); console.log(name,'initial boot');await page.goto(origin);await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#carModeBtn')); await wait(page,async()=>!!(await navigator.serviceWorker.getRegistration())?.active); await page.evaluate(()=>Lazy.warm()); await wait(page,async()=>{const c=await caches.open('ytplayer-assets'),r=await c.match('/__ytp_asset_state');if(!r)return false;const s=await r.json();return !(await Promise.all(Object.values(s.current.groups).flatMap(g=>g.files).map(async p=>!!await c.match(p+'?v='+s.current.files[p].h)))).includes(false);}); console.log(name,'warm complete');assert.equal(await page.evaluate(()=>Lazy.loaded('feature:car-mode')),false); assert.equal(await page.evaluate(()=>[...document.querySelectorAll('.stg-overlay,.deck-chips,.vol-pct,.mode-name,.stg-bot,.ch-badge,.upnext-clear,.list-sub,.p2p-strip,.sleep-status .sl-ic,.sleep-status .sl-sub,.mc-tune')].every(n=>getComputedStyle(n).display==='none')),true,'classic retains shared visibility defaults without loading Glass CSS'); await checkCssOrder(page); assert.equal(await page.evaluate(()=>Lazy.loaded('feature:piano')),false); assert.equal(await page.evaluate(()=>[typeof newPlaylist,typeof renderSettings,typeof enterServiceMode,typeof Notes.load,typeof LyricsCore.fromText].every(type=>type==='function')),true,'playlist, settings, service mode and lyric core remain in RAM'); // Profile/imported settings use applyAppearance, not the Settings select handler. await page.evaluate(()=>{data.settings.layout='glass-stage';applyAppearance();}); await wait(page,()=>document.documentElement.dataset.layout==='glass-stage'&&Lazy.layout==='glass-stage'); await page.evaluate(()=>{data.settings.layout='classic';applyAppearance();}); await wait(page,()=>document.documentElement.dataset.layout==='classic'&&Lazy.layout==='classic'); // Eager application panels stay usable without executing optional settings modules. await page.click('[data-view="settings"]');await page.click('[data-section="appearance"]');await page.waitForSelector('#setLayout'); assert.equal(await page.evaluate(()=>Lazy.loaded('feature:piano')),false); await page.selectOption('#setLayout','glass-stage');await page.waitForFunction(()=>document.documentElement.dataset.layout==='glass-stage'); await page.evaluate(()=>{window.__saveData=true;localStorage.setItem('perf-save-data','1');localStorage.setItem('perf-offline','1');}); await stop();disconnected=true;await proxy.setOffline(true); console.log(name,'offline reload');await page.reload();await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#carModeBtn')); assert.equal(await page.evaluate(()=>document.documentElement.dataset.layout),'glass-stage'); assert.equal(await page.evaluate(()=>window.__firstBodyLayout),'glass-stage','selected layout is established before body/first paint'); await checkCssOrder(page); const before=proxy.getStats().requestCount; await page.getByRole('button',{name:'Car mode',exact:true}).click();await page.waitForSelector('.car-mode'); assert.equal(await page.locator('.car-controls button').count(),4,'all transport controls survive lazy setup'); assert.equal(await page.locator('.car-controls #voiceSearchBtn').count(),0,'voice search stays outside transport'); await page.getByRole('button',{name:'Exit Car mode'}).click(); assert.equal(proxy.getStats().requestCount,before,'warmed feature first use reaches no network'); await page.click('[data-view="settings"]');await page.click('[data-section="appearance"]');await page.selectOption('#setLayout','bento-hub');await page.waitForFunction(()=>document.documentElement.dataset.layout==='bento-hub'); await page.selectOption('#setLayout','f7-swipe');await page.waitForFunction(()=>document.documentElement.dataset.layout==='f7-swipe'&&!!window.Framework7); await page.selectOption('#setLayout','classic');await page.waitForFunction(()=>document.documentElement.dataset.layout==='classic'); assert.equal(proxy.getStats().requestCount,before,'offline layout changes reach no network'); await page.getByRole('searchbox',{name:'Search settings and controls'}).fill('Enable piano transcription'); await page.locator('.settings-results button').first().waitFor(); await page.getByRole('searchbox',{name:'Search settings and controls'}).fill(''); // Every extension panel renders from disk without executing other panels eagerly. for(const id of ['piano','midi','floating-lyrics','vocal-reducer','equalizer','setlist-import','broadcast']) { await page.evaluate(id=>document.querySelector('[data-section="'+id+'"]').click(),id); await wait(page,async id=>{const section=SettingsSections.get(id);return !section.lazyGroup;},id); } await page.evaluate(async()=>{await Lazy.load('feature:export');await Lazy.load('feature:video-edit');}); assert.equal(proxy.getStats().requestCount,before,'all warmed extension assets stay offline'); await start();disconnected=false;await proxy.setOffline(false);await page.evaluate(()=>localStorage.removeItem('perf-offline')); // Keep an old tab whose core and group contracts remain those of build N. const old=await context.newPage();old.on('pageerror',e=>errors.push({message:e.message,disconnected}));await old.goto(origin);await old.waitForSelector('#carModeBtn'); await old.evaluate(async()=>{window.__saveData=true;await Lazy.load('feature:setlist-import');window.__pinned=window.SetlistImport;}); await page.evaluate(()=>window.__saveData=true); const previous=await page.evaluate(()=>Lazy.manifest); await stop(); for(const file of ['video-edit.js','setlist-import.js','car-mode.js'])writeFileSync(join(cwd,'public',file),readFileSync(join(cwd,'public',file),'utf8')+'\n// phase3 new file\n'); const defs=JSON.parse(readFileSync(join(cwd,'public/assets.json')));defs.groups['feature:car-mode'].contract=2;writeFileSync(join(cwd,'public/assets.json'),JSON.stringify(defs)); // Production stamping replaces the source-only native map with these updated groups. await start();const target=await(await fetch(origin+'/api/manifest')).json(); await page.evaluate(async()=>{window.__saveData=true;const r=await navigator.serviceWorker.getRegistration();await r.update();}); await wait(page,async target=>{const r=await navigator.serviceWorker.getRegistration();if(!r?.waiting)return false;return(await SwUpdate.askCacheStatus(r.waiting))?.version===target;},target.buildTag); // No automatic navigation; a real playing flag blocks the real apply flow. let navs=0;page.on('framenavigated',f=>{if(f===page.mainFrame())navs++;}); await sleep(1000);assert.equal(navs,0); await page.evaluate(()=>Player._wantsPlaying=true); page.once('dialog',d=>d.dismiss()); const guard=await page.evaluate(async()=>{try{await SwUpdate.applyUpdate({getRegistration:()=>navigator.serviceWorker.getRegistration(),askStatus:SwUpdate.askCacheStatus,reload:()=>location.reload()});return false;}catch{return true;}}); assert.equal(guard,true);assert.equal(navs,0); await page.evaluate(()=>Player._wantsPlaying=false); // Explicit worker message, then explicit navigation into new stamped core. await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.waiting.postMessage({type:'SKIP_WAITING'});}); await wait(page,async tag=>{const c=await caches.open('ytplayer-assets');return(await(await c.match('/__ytp_asset_state')).json()).current.buildTag===tag;},target.buildTag); await page.reload();await page.waitForSelector('#carModeBtn');assert.equal(await page.evaluate(()=>Lazy.manifest.buildTag),target.buildTag); // No warm: the new page gets a same-contract N-1 file immediately from disk. const cacheKey='/video-edit.js?v='+target.files['/video-edit.js'].h; assert.equal(await page.evaluate(async key=>!!await(await caches.open('ytplayer-assets')).match(key),cacheKey),false); const stale=await page.evaluate(async key=>{const r=await fetch(key);return {h:r.headers.get('X-Asset-Hash'),body:await r.text()};},cacheKey); assert.equal(stale.h,previous.files['/video-edit.js'].h);assert.ok(!stale.body.includes('// phase3 new file')); await page.evaluate(()=>Lazy.load('feature:video-edit'));const pinned=await page.evaluate(()=>{window.__videoPinned=VideoEdit;return Lazy.loaded('feature:video-edit');});assert.equal(pinned,true); // Different contract is already downloaded before commit, but the old core must keep N-1. const oldRequests=[];old.on('request',r=>oldRequests.push(r.url())); await old.evaluate(()=>Lazy.load('feature:car-mode'));assert.equal(await old.evaluate(()=>Lazy.reloadRequired),true); await old.getByRole('button',{name:'Refresh UI',exact:true}).waitFor(); assert.ok(oldRequests.some(u=>u.includes('/car-mode.js?v='+previous.files['/car-mode.js'].h))); await old.evaluate(()=>Lazy.load('feature:setlist-import'));assert.equal(await old.evaluate(()=>window.SetlistImport===window.__pinned),true); // Warm new files explicitly; compatible, unexecuted old-page group selects new cached URL. await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.active.postMessage({type:'WARM_ASSETS',saveData:false});}); await wait(page,async key=>!!await(await caches.open('ytplayer-assets')).match(key),cacheKey); await old.evaluate(()=>Lazy.load('feature:video-edit'));assert.ok(oldRequests.some(u=>u.includes(cacheKey))); await page.evaluate(()=>Lazy.load('feature:video-edit'));assert.equal(await page.evaluate(()=>window.VideoEdit===window.__videoPinned),true); const offlineTransportErrors=errors.filter(error=>name==='webkit'&&error.disconnected&&(error.message==='TypeError: Load failed'||/\/api\/(?:recommendations\?[^\s]*|version)\.$/.test(error.message))); assert.deepEqual(errors.filter(error=>!offlineTransportErrors.includes(error)),[]); if(offlineTransportErrors.length)assert.ok(failedRequests.some(url=>/\/api\/(?:recommendations|version)/.test(url))); results.push({browser:name,sourceBuildTag:previous.buildTag,classicSharedDefaults:true,cssOrderPreserved:true,settingsSearch:true,carControls:4,offlineFeature:true,firstUseNetwork:0,layoutsOffline:['glass-stage','bento-hub','f7-swipe','classic'],pinned:true,compatibleUnexecutedNewURL:true,incompatibleN1ReloadRequired:true,stagedN1:true,playbackGuard:true,prePaintLayout:true,offlineSettingsPanels:true,scriptErrors:[],offlineTransportErrors}); console.log(name,'PASS'); } finally {await browser?.close();await proxy?.close();await stop();} } try{for(const name of requested==='all'?['chromium','webkit']:[requested])await run(name);writeFileSync(args.includes('--out')?resolve(args[args.indexOf('--out')+1]):join(repo,'perf/results/phase3-lazy-2026-10-08.json'),JSON.stringify({results},null,2)+'\n');}finally{rmSync(scratch,{recursive:true,force:true});}