Files
ytplayer/frontend/lazy.test.js

79 lines
6.0 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const vm = require('node:vm');
function fixture(current, held = []) {
const inserted = [], listeners = {}, writes = [];
const manifest = { buildTag: 'own', groups: { core: { files: [] }, 'layout:classic': { files: ['/classic.css'] }, 'layout:glass-stage': { files: ['/glass.css', '/one.js', '/two.js'] }, 'feature:test': { contract:1, files: ['/one.js', '/two.js'] } }, files: { '/classic.css': {h:'c'}, '/glass.css':{h:'g'}, '/one.js':{h:'1'}, '/two.js':{h:'2'} } };
const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f };
doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } };
const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, setTimeout, clearTimeout, navigator:{}, addEventListener(){} };
if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) };
root.window=root; root.globalThis=root;
vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root);
return { root, inserted, writes, listeners, manifest };
}
test('external head bootstrap selects remembered CSS before paint without fetching a manifest', () => {
const {root,writes}=fixture();
assert.equal(root.document.documentElement.dataset.layout,'glass-stage');
assert.deepEqual(writes,['<link rel="stylesheet" href="/glass.css?v=g" data-lazy-css="/glass.css">']);
});
test('lazy groups execute ordered classic scripts once, sharing concurrent calls and dependencies', async () => {
const {root,inserted}=fixture();
const one=root.Lazy.load('feature:test'), two=root.Lazy.load('feature:test');
await Promise.all([one,two]);
assert.deepEqual(inserted.map(n=>n.src),['/one.js?v=1','/two.js?v=2']);
assert.equal(root.Lazy.loaded('feature:test'),true);
await root.Lazy.load('layout:glass-stage');
assert.equal(inserted.filter(n=>n.tagName==='SCRIPT').length,2);
});
test('failed injection can retry and proxy dispatches only after execution', async () => {
const {root,inserted}=fixture();
const append=root.document.head.append;
root.document.head.append=n=>{inserted.push(n);queueMicrotask(()=>n.onerror?.());};
await assert.rejects(root.Lazy.load('feature:test'),/one.js/);
root.document.head.append=append;
root.TestApi={open:value=>value+1};
const proxy=root.Lazy.proxy('feature:test',['open'],'TestApi');
assert.equal(await proxy.open(4),5);
assert.equal(root.Lazy.url('/two.js'),'/two.js?v=2');
await assert.rejects(root.Lazy.load('feature:missing'),/Unknown/);
});
test('executing groups are pinned even after a newer compatible manifest arrives', async () => {
const f=fixture(); await f.root.Lazy.load('feature:test');
const newer=JSON.parse(JSON.stringify(f.manifest)); newer.files['/one.js'].h='new';
f.root.caches={open:async()=>({match:async key=>key==='/__ytp_asset_state'?new Response(JSON.stringify({current:newer})):new Response('',{headers:{'X-Asset-Hash':'new'}})})};
await f.root.Lazy.load('feature:test');
assert.deepEqual(f.inserted.map(n=>n.src),['/one.js?v=1','/two.js?v=2']);
});
test('unexecuted compatible group immediately uses newly cached file URLs', async () => {
const next=fixture().manifest; next.buildTag='new'; next.files['/one.js'].h='new';
const f=fixture(next,['/one.js?v=new','/two.js?v=2']);
await f.root.Lazy.load('feature:test');
assert.deepEqual(f.inserted.map(n=>n.src),['/one.js?v=new','/two.js?v=2']);
assert.equal(f.root.Lazy.reloadRequired,false);
});
test('unexecuted incompatible group keeps its running-build N-1 URLs and requires reload', async () => {
const next=fixture().manifest; next.buildTag='new'; next.files['/one.js'].h='new'; next.groups['feature:test'].contract=2;
const f=fixture(next,['/one.js?v=new','/two.js?v=2']);
const events=[]; f.root.CustomEvent=class {constructor(type,opts){this.type=type;this.detail=opts.detail}}; f.root.dispatchEvent=e=>events.push(e.type);
await f.root.Lazy.load('feature:test');
assert.deepEqual(f.inserted.map(n=>n.src),['/one.js?v=1','/two.js?v=2']);
assert.equal(f.root.Lazy.reloadRequired,true);
assert.ok(events.includes('ytp-reload-required'));
});
test('workers receive the pinned import URL map in their first message', () => {
const f=fixture(), sent=[]; f.root.Worker=class { constructor(url){this.url=url;} postMessage(message,transfer){sent.push({message,transfer});} };
f.manifest.files['/worker.js']={h:'w'}; f.root.Lazy.manifest.files['/worker.js']={h:'w'};
const worker=f.root.Lazy.worker('/worker.js');worker.postMessage({op:'open'},['transfer']);worker.postMessage({op:'finish'});
assert.equal(worker.url,'/worker.js?v=w');assert.equal(sent[0].message.assetUrls.sha256,f.root.Lazy.url('/sha256.js'));assert.equal(sent[0].message.assetUrls.wasm,f.root.Lazy.url('/sha256-wasm.js'));assert.deepEqual(sent[0].transfer,['transfer']);assert.equal(sent[1].message.assetUrls,undefined);
});
test('new group files with a changed dependency contract cannot run against the old dependency',async()=>{
const next=fixture().manifest;next.buildTag='next';next.groups['feature:test'].dependencies=['feature:dep'];next.groups['feature:dep']={contract:2,files:[]};next.files['/one.js'].h='new';
const f=fixture(next,['/one.js?v=new','/two.js?v=2']);f.root.Lazy.manifest.groups['feature:test'].dependencies=['feature:dep'];f.root.Lazy.manifest.groups['feature:dep']={contract:1,files:[]};
await f.root.Lazy.load('feature:test');assert.deepEqual(f.inserted.map(n=>n.src),['/one.js?v=1','/two.js?v=2']);assert.equal(f.root.Lazy.reloadRequired,true);
});