Files
ytplayer/perf/seams.mjs

77 lines
7.1 KiB
JavaScript

#!/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/seams-'));
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()<end){if(await page.evaluate(fn,arg))return;await sleep(100);}throw Error('Timed out: '+fn.toString());}
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();
await context.route('**/*',r=>r.request().url().startsWith(origin)?r.continue():r.abort());
await context.addInitScript(()=>{Object.defineProperty(navigator,'onLine',{configurable:true,get:()=>!window.__offline});});
const page=await context.newPage(),errors=[];page.on('pageerror',e=>errors.push(e.message));
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);});
await page.reload();await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#carModeBtn'));
await page.evaluate(()=>{window.__offline=true;window.__shellBuild=document.querySelector('meta[name="ytp-build"]').content;});
await stop();await proxy.setOffline(true);const before=proxy.getStats().requestCount;
await page.evaluate(()=>{data.playlists=[{id:'smoke',name:'Cached playlist',videos:[]}];render();});
assert.equal(await page.locator('#homePlGrid .home-pl-name').textContent(),'Cached playlist');
await page.click('[data-view="settings"]');await page.waitForSelector('.settings');
await page.evaluate(()=>{view={type:'search'};render();});
const groups=await page.evaluate(()=>Object.keys(Lazy.manifest.groups));
const checked=['shell'];
if(groups.includes('feature:presenter')) {
await page.evaluate(async()=>{history.replaceState(null,'','/?present=');await Presenter.boot();});
await page.waitForSelector('#presenterView:not(.hidden)');await page.keyboard.press('b');
assert.equal(await page.locator('#pvBlank').evaluate(n=>!n.classList.contains('hidden')),true);
await page.evaluate(()=>{document.querySelector('#presenterView').classList.add('hidden');document.body.classList.remove('presenter-open');});checked.push('presenter');
}
if(groups.includes('feature:remote-client')) {
await page.evaluate(()=>document.querySelector('#navRemoteBtn').click());
await page.waitForSelector('#modal:not(.hidden)');assert.match(await page.locator('#modal').textContent(),/remote|screen|phone/i);
await page.evaluate(()=>closeModal());checked.push('remote');
}
if(groups.includes('feature:watch-party')) {
await page.evaluate(()=>document.querySelector('#navPartyBtn').click());await page.waitForSelector('#partyPanel:not(.hidden)');
await page.locator('.pt-name').fill('Cached guest');assert.equal(await page.locator('.pt-name').inputValue(),'Cached guest');checked.push('party');
}
if(groups.includes('feature:share-external')) {
await page.evaluate(async()=>{current={meta:{id:'benchmedia01',title:'Cached smoke',channel:'Fixture'}};await Share.open();});
await page.waitForSelector('.share-moment');assert.match(await page.locator('.sm-app').textContent(),/v=benchmedia01/);
await page.locator('.sm-time').fill('0:30');await page.locator('.sm-time').dispatchEvent('input');
assert.match(await page.locator('.sm-app').textContent(),/t=30/);
const m3u=await page.evaluate(async()=>{await Lazy.load('feature:share-external');return window.YT.External.m3u([{id:'benchmedia01',title:'Cached smoke'}]);});assert.match(m3u,/#EXTM3U/);assert.match(m3u,/benchmedia01/);
await page.evaluate(()=>closeModal());checked.push('share');
}
if(groups.includes('feature:stats-view')) {
await page.evaluate(async()=>{view={type:'stats'};render();});await page.waitForSelector('.stats-empty');checked.push('stats');
}
if(groups.includes('feature:share-external')) {
await page.evaluate(()=>{Share.open=()=>Promise.reject(new Error('Unavailable fixture chunk'));document.querySelector('#shareMomentBtn').click();});
await page.getByText('Unavailable fixture chunk',{exact:true}).waitFor();
}
assert.equal(proxy.getStats().requestCount,before,'all warmed seam code loads from SW without network');
// Ignore only verified disconnected version/recommendation API transports in WebKit.
const scriptErrors=errors.filter(e=>!(name==='webkit'&&(e==='TypeError: Load failed'||/\/api\/(?:recommendations\?[^\s]*|version)\.$/.test(e))));assert.deepEqual(scriptErrors,[]);
results.push({browser:name,checked,offlineNetworkRequests:proxy.getStats().requestCount-before,errors:scriptErrors});console.log(name,JSON.stringify(results.at(-1)));
}finally{await browser?.close();await stop();await proxy?.close();}
}
try{for(const name of requested==='all'?['chromium','webkit']:[requested])await run(name);writeFileSync(join(repo,'perf/results/phase4-seams-2026-10-08.json'),JSON.stringify(results,null,2)+'\n');}finally{rmSync(scratch,{recursive:true,force:true});}