Files
Endoleap/tests/headless/browser-storage.mjs

71 lines
5.2 KiB
JavaScript

/** Real Chromium IndexedDB/Web Locks checks, in an isolated temporary browser profile. */
import { createServer } from 'node:http';
import { readFile, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
import { spawn } from 'node:child_process';
import { stripTypeScriptTypes } from 'node:module';
import { setTimeout as pause } from 'node:timers/promises';
const root = resolve(import.meta.dirname, '../..'), profile = await mkdtemp(join(tmpdir(), 'endoleap-storage-browser-'));
const packageMode=process.argv.includes('--package');
const readStreams=new Map();
const server = createServer(async (request, response) => {
try {
const path = new URL(request.url, 'http://localhost').pathname;
if(path==='/loader-stream'){
const url=new URL(request.url,'http://localhost'),key=url.searchParams.get('key');
if(!key){response.writeHead(400).end();return;}
const state=readStreams.get(key)??{reads:0};state.reads++;state.response=response;readStreams.set(key,state);
response.writeHead(200,{'Content-Type':'application/x-ndjson'});response.write(JSON.stringify({part:0,complete:false})+'\n');return;
}
if(path==='/loader-release'){
const url=new URL(request.url,'http://localhost'),state=readStreams.get(url.searchParams.get('key'));
if(!state){response.writeHead(404).end();return;}
if(!state.response.destroyed)state.response.end(url.searchParams.get('fail')==='1'?'{invalid tail}\n':JSON.stringify({part:1,complete:true})+'\n');
response.setHeader('Content-Type','application/json');response.end(JSON.stringify({reads:state.reads}));return;
}
if (path === '/') { response.setHeader('Content-Type', 'text/html'); response.end('<title>Headless storage checks</title>'); return; }
const isPackage=packageMode&&path.startsWith('/package/');
const filename = isPackage?resolve(root,'.headless-dist',path.slice('/package/'.length)):resolve(root, `.${path}`);
if (!(isPackage&&filename.startsWith(`${root}/.headless-dist/`)) && !filename.startsWith(`${root}/src/lib/`) && !filename.startsWith(`${root}/tests/headless/`)) { response.writeHead(404).end(); return; }
let source = await readFile(filename, 'utf8');
if (filename.endsWith('.ts')) source = stripTypeScriptTypes(source);
response.setHeader('Content-Type', 'text/javascript'); response.end(source);
} catch (error) { response.writeHead(500).end(String(error)); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const chrome = spawn(process.env.CHROME_BIN ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', ['--headless=new', '--no-first-run', '--disable-background-networking', '--disable-component-update', '--remote-debugging-port=0', `--user-data-dir=${profile}`, `http://127.0.0.1:${server.address().port}/`], { stdio: 'ignore' });
let socket;
try {
let port;
for (let attempt = 0; attempt < 100; attempt++) {
try { port = Number((await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0]); break; } catch { await pause(100); }
}
if (!port) throw new Error('isolated Chromium did not start');
const tabs = await fetch(`http://127.0.0.1:${port}/json/list`).then(response => response.json());
socket = new WebSocket(tabs.find(tab => tab.type === 'page').webSocketDebuggerUrl);
await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject; });
let next = 0; const pending = new Map();
socket.onmessage = event => { const message = JSON.parse(event.data); if (message.id) { const callbacks = pending.get(message.id); pending.delete(message.id); if (message.error) callbacks.reject(new Error(JSON.stringify(message.error))); else callbacks.resolve(message.result); } };
const call = (method, params) => new Promise((resolve, reject) => { const id = ++next; pending.set(id, { resolve, reject }); socket.send(JSON.stringify({ id, method, params })); });
const origin = `http://127.0.0.1:${server.address().port}`;
await call('Page.navigate', { url: `${origin}/` });
let loaded = false;
for (let attempt = 0; attempt < 100; attempt++) {
const state = await call('Runtime.evaluate', { expression: `location.origin === ${JSON.stringify(origin)} && document.readyState === 'complete'`, returnByValue: true });
if (state.result?.value === true) { loaded = true; break; }
await pause(50);
}
if (!loaded) throw new Error('browser test origin did not finish loading');
const result = await call('Runtime.evaluate', { expression: `import('/tests/headless/${packageMode?'package-browser-cases':'browser-storage-cases'}.mjs').then(module => module.run())`, awaitPromise: true, returnByValue: true, timeout: 30000 });
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text);
for (const message of result.result.value) console.log(`PASS ${message}`);
} finally {
socket?.close();
const exited = new Promise(resolve => { if (chrome.exitCode !== null) resolve(); else chrome.once('exit', resolve); });
chrome.kill(); await exited;
await new Promise(resolve => server.close(resolve));
await rm(profile, { recursive: true, force: true, maxRetries: 3 });
}