Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
124 lines
7.4 KiB
JavaScript
124 lines
7.4 KiB
JavaScript
import {build} from 'esbuild';
|
|
import {chromium} from 'playwright';
|
|
import {createServer} from 'node:http';
|
|
import {once} from 'node:events';
|
|
import {readFile, writeFile} from 'node:fs/promises';
|
|
import {createHash} from 'node:crypto';
|
|
import {fileURLToPath} from 'node:url';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const dir = fileURLToPath(new URL('.', import.meta.url));
|
|
const bundle = await build({entryPoints: [dir + 'fixture.jsx'], bundle: true, write: false,
|
|
define: {'process.env.NODE_ENV': '"development"'}, format: 'iife'});
|
|
const server = createServer((req, res) => {
|
|
if (req.url === '/bundle.js') {
|
|
res.writeHead(200, {'Content-Type': 'application/javascript'}); res.end(bundle.outputFiles[0].contents);
|
|
} else if (req.url === '/failure') {
|
|
res.writeHead(503, {'Content-Type': 'text/plain'}); res.end('synthetic unavailable');
|
|
} else {
|
|
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
res.end('<!doctype html><html lang="en"><head><title>Boundary probe</title></head><body><div id="root"></div><script src="/bundle.js"></script></body></html>');
|
|
}
|
|
});
|
|
server.listen(0, '127.0.0.1'); await once(server, 'listening');
|
|
const url = `http://127.0.0.1:${server.address().port}`;
|
|
let browser;
|
|
const cases = [];
|
|
async function probe(id, query, action) {
|
|
const page = await browser.newPage();
|
|
page.setDefaultTimeout(5000);
|
|
const browserErrors = [];
|
|
page.on('pageerror', e => browserErrors.push(e.message));
|
|
try {
|
|
await page.goto(url + query);
|
|
await page.getByRole('button').first().waitFor();
|
|
const observed = await action(page);
|
|
cases.push({id, status: 'pass', ...observed, browserErrors});
|
|
} catch (error) {
|
|
cases.push({id, status: 'fail', error: error.message, observed: await page.evaluate(() => window.observed).catch(() => null), browserErrors});
|
|
} finally {await page.close();}
|
|
}
|
|
async function active(page, id) {
|
|
try { await page.waitForFunction(id => document.activeElement?.id === id, id, {timeout: 5000}); }
|
|
catch (error) { throw new Error(`Expected focus ${id}; got ${await page.evaluate(() => ({id: document.activeElement?.id, tag: document.activeElement?.tagName, dialogs: document.querySelectorAll('[role=dialog]').length})).then(JSON.stringify)}: ${error.message}`); }
|
|
}
|
|
try {
|
|
browser = await chromium.launch({headless: true,
|
|
...(process.env.FEP_CHROMIUM_PATH ? {executablePath: process.env.FEP_CHROMIUM_PATH} : {})});
|
|
await probe('X01-render-contained', '/', async page => {
|
|
await page.getByText('Break render', {exact: true}).click();
|
|
await page.getByTestId('inner').waitFor();
|
|
await page.getByText('Sibling 0', {exact: true}).click();
|
|
await page.getByText('Sibling 1', {exact: true}).waitFor();
|
|
const o = await page.evaluate(() => window.observed);
|
|
assert(o.caught.some(x => x.boundary === 'inner' && x.message === 'render fault'));
|
|
assert.equal(await page.getByTestId('outer').count(), 0);
|
|
return {observed: o, siblingUsable: true};
|
|
});
|
|
await probe('X02-event-not-contained', '/', async page => {
|
|
await page.getByText('Break event', {exact: true}).click();
|
|
await page.waitForFunction(() => window.observed.globalErrors.some(x => x.includes('event fault')));
|
|
const o = await page.evaluate(() => window.observed);
|
|
assert.equal(o.caught.length, 0); assert.equal(await page.getByTestId('panel').count(), 1);
|
|
return {observed: o, panelPresent: true};
|
|
});
|
|
await probe('X03-request-explicit-state', '/', async page => {
|
|
await page.getByText('Fail request', {exact: true}).click();
|
|
await page.getByRole('status').filter({hasText: 'request failed'}).waitFor();
|
|
const o = await page.evaluate(() => window.observed);
|
|
assert.deepEqual(o.requests, [503]); assert.equal(o.caught.length, 0);
|
|
return {observed: o, state: 'request failed'};
|
|
});
|
|
await probe('X04-fallback-escapes-inner', '/?brokenFallback=1', async page => {
|
|
await page.getByText('Break render', {exact: true}).click();
|
|
await page.getByTestId('outer').waitFor();
|
|
const o = await page.evaluate(() => window.observed);
|
|
assert(o.caught.some(x => x.boundary === 'outer' && x.message === 'fallback fault'));
|
|
return {observed: o, enclosingFallback: true};
|
|
});
|
|
await probe('X05-focus-existing-invoker', '/?mode=focus', async page => {
|
|
await page.locator('#invoker').focus(); await page.keyboard.press('Enter');
|
|
await active(page, 'remove'); await page.keyboard.press('Escape'); await active(page, 'invoker');
|
|
return {activeId: await page.evaluate(() => document.activeElement.id)};
|
|
});
|
|
await probe('X06-focus-removed-default', '/?mode=focus', async page => {
|
|
await page.locator('#invoker').focus(); await page.keyboard.press('Enter'); await active(page, 'remove');
|
|
await page.keyboard.press('Enter'); await page.getByRole('dialog').waitFor({state: 'detached'});
|
|
await page.waitForFunction(() => document.activeElement === document.body);
|
|
// Negative control: default restoration cannot choose a workflow target for the removed trigger.
|
|
assert.equal(await page.locator('#invoker').count(), 0);
|
|
return {activeTag: await page.evaluate(() => document.activeElement.tagName), workflowTargetSelected: false};
|
|
});
|
|
await probe('X07-focus-removed-explicit', '/?mode=focus&explicit=1', async page => {
|
|
await page.locator('#invoker').focus(); await page.keyboard.press('Enter'); await active(page, 'remove');
|
|
await page.keyboard.press('Enter'); await active(page, 'next');
|
|
return {activeId: await page.evaluate(() => document.activeElement.id), applicationPolicy: true};
|
|
});
|
|
await probe('X08-nested-return', '/?mode=focus', async page => {
|
|
await page.locator('#invoker').focus(); await page.keyboard.press('Enter'); await active(page, 'remove');
|
|
await page.keyboard.press('Tab'); await active(page, 'nested-invoker');
|
|
await page.keyboard.press('Enter'); await active(page, 'nested-close');
|
|
// Experimental timing variable, not a hidden retry: compare immediate versus settled Escape.
|
|
if (Number(process.env.FEP_NESTED_SETTLE_MS)) await page.waitForTimeout(Number(process.env.FEP_NESTED_SETTLE_MS));
|
|
await page.keyboard.press('Escape'); await active(page, 'nested-invoker');
|
|
assert.equal(await page.getByRole('dialog').count(), 1);
|
|
await page.keyboard.press('Escape'); await active(page, 'invoker');
|
|
return {innerReturn: 'nested-invoker', outerReturn: 'invoker', observed: await page.evaluate(() => window.observed)};
|
|
});
|
|
const files = {};
|
|
for (const name of ['fixture.jsx', 'experiment.mjs', 'package.json', 'package-lock.json']) {
|
|
files[name] = createHash('sha256').update(await readFile(dir + name)).digest('hex');
|
|
}
|
|
const result = {date: new Date().toISOString(), evidence: 'synthetic-browser-fixture-real-libraries',
|
|
node: process.version, browser: browser.version(), nestedSettleMs: Number(process.env.FEP_NESTED_SETTLE_MS || 0),
|
|
browserSelection: process.env.FEP_CHROMIUM_PATH ? 'explicit executable override' : 'playwright bundled',
|
|
packages: JSON.parse(await readFile(dir + 'package.json')).dependencies,
|
|
files, cases, passed: cases.filter(x => x.status === 'pass').length, total: cases.length};
|
|
const output = process.env.FEP_RESULT_PATH || '/tmp/fep-boundary-result.json';
|
|
await writeFile(output, JSON.stringify(result, null, 2) + '\n');
|
|
console.log(JSON.stringify({output, passed: result.passed, total: result.total, cases}, null, 2));
|
|
if (result.passed !== result.total) process.exitCode = 1;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
await new Promise(resolve => server.close(resolve));
|
|
}
|