repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/ui-test.mjs
Rawimport assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { stripVTControlCharacters } from 'node:util';
import { setTimeout as delay } from 'node:timers/promises';
import { prepareNiriIntegration, runNiriIntegration } from './test-fixtures/ui/niri-integration.mjs';
import { taskSelection, taskOverlayFixtures, taskEntryFixture, runTasks } from './test-fixtures/ui/tasks.mjs';
import { runResourceProbe } from './test-fixtures/ui/resource-probe.mjs';
const project = dirname(fileURLToPath(import.meta.url));
export function childEnvironment(runtime, display, socket) {
const directory = realpathSync(runtime);
for (const path of [display, socket]) {
assert.equal(dirname(realpathSync(path)), directory, 'refusing non-child socket');
assert(lstatSync(path).isSocket(), 'child endpoint must be a socket');
}
assert.match(basename(display), /^wayland-\d+$/);
assert.match(basename(socket), /^niri\..*\.sock$/);
return { ...process.env, WAYLAND_DISPLAY: display, NIRI_SOCKET: socket, XDG_RUNTIME_DIR: directory };
}
export function assertCleanLog(text) {
assert(!/\b(?:WARN(?:ING)?|ERROR)\b|Error parsing|binding loop|ReferenceError|TypeError|deprecated|Could not|failed/i.test(text), text);
}
// Owner-approved sandbox exceptions only. Preserve raw logs and fail every
// other diagnostic, including these same messages at another level or source.
export function assertNiriLog(text, writeFailure = null) {
const fallback = 'failed building default dmabuf feedback, falling back to v3: error getting EGL device render node\n\nCaused by:\n None of the following EGL extensions is supported by the underlying EGL implementation, at least one is required: ["EGL_EXT_device_drm"]';
const saving = 'saving screenshot to "/evidence/missing-directory/native-failure.png"';
const failedWrite = 'error saving screenshot image: Os { code: 2, kind: NotFound, message: "No such file or directory" }';
if (writeFailure) {
assert(Number.isSafeInteger(writeFailure.start) && Number.isSafeInteger(writeFailure.end));
assert(writeFailure.start >= 0 && writeFailure.start < writeFailure.end && writeFailure.end <= text.length, 'invalid expected diagnostic interval');
for (const offset of [writeFailure.start, writeFailure.end])
assert(offset === 0 || text[offset - 1] === '\n', 'diagnostic interval must end on record boundaries');
}
const parts = writeFailure ? [text.slice(0, writeFailure.start), text.slice(writeFailure.start, writeFailure.end), text.slice(writeFailure.end)] : [text];
const result = { softwareFallbacks: 0, expectedWriteFailures: 0 };
for (const [index, part] of parts.entries()) {
let previous = null;
const records = stripVTControlCharacters(part).split(/(?=^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z\s+(?:TRACE|DEBUG|INFO|WARN|ERROR)\s)/m);
for (const record of records) {
const match = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+(\S+): ([\s\S]*?)\s*$/.exec(record);
const [, level, source, message] = match || [];
if (level === 'DEBUG' && source === 'niri::backend::winit' && message === fallback) {
assert(++result.softwareFallbacks <= 1, 'duplicate software fallback');
} else if (writeFailure && index === 1 && level === 'WARN' && source === 'niri::niri' && message === failedWrite) {
assert.deepEqual(previous, ['DEBUG', 'niri::niri', saving], 'write warning must follow the deliberate destination');
assert(++result.expectedWriteFailures === 1, 'duplicate expected write warning');
} else assertCleanLog(record);
previous = match ? [level, source, message] : null;
}
}
assert.equal(result.expectedWriteFailures, writeFailure ? 1 : 0, 'missing deliberate write warning');
return result;
}
export function assertPureService(text) {
assert.match(text, /^import QtQuick\s+Item\s*\{/);
assert.equal((text.match(/\bimport\b/g) || []).length, 1, 'mock imports must be QtQuick only');
assert.deepEqual(text.match(/\b[A-Z]\w*\s*\{/g), ['Item {'], 'mock must contain only Item state/functions');
assert(!/\b(?:Qt|Quickshell|Process|FileView|XMLHttpRequest|WebSocket|WorkerScript|PolkitAgent|PamContext|fetch|eval|Function)\b/.test(text), 'privileged or dynamic mock API');
}
export async function stopChild(child) {
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
const closed = new Promise(resolve => child.once('close', resolve));
child.kill('SIGTERM');
const force = setTimeout(() => child.kill('SIGKILL'), 2000);
try { await closed; } finally { clearTimeout(force); }
}
export function replayRequest(args) {
const fixture = args[0] ?? 'all';
const size = args[1];
assert(args.length <= 2 && ['all', 'controls', 'popouts', 'system', 'privilege', 'noko', 'niri', taskSelection].includes(fixture)
&& (size === undefined || fixture === taskSelection && ['1366x768', '2560x1440'].includes(size)),
'usage: just ui-test [all|controls|popouts|system|privilege|noko|niri|tasks] [tasks: 1366x768|2560x1440]');
return { version: 1, fixture, command: `just ui-test ${fixture}${size ? ` ${size}` : ''}`, ...(size ? { size } : {}) };
}
export function assertSafeCapture(snapshot) {
assert(snapshot, 'snapshot unavailable for safe screenshot');
if (snapshot.auth?.visible) {
assert(snapshot.auth.hasInput === false || snapshot.auth.inputMasked === true, 'refusing screenshot of unmasked input');
}
}
export async function captureUiFailure(error, { evidence, fixture, phase, snapshot, screenshot, write = writeFileSync }) {
const record = { version: 1, fixture, phase, error: error.stack || String(error), snapshot, screenshot: null, captureErrors: [] };
try { record.screenshot = await screenshot('failure'); }
catch (captureError) { record.captureErrors.push(captureError.stack || String(captureError)); }
write(join(evidence, 'failure.json'), JSON.stringify(record, null, 2));
return record;
}
export async function finishUiRun(failure, { children, checkLogs, capture, stop = stopChild, report = error => console.error(error.stack || String(error)) }) {
let captured = false;
async function retainFailure() {
if (!failure || captured) return;
captured = true;
try { await capture(failure); } catch (error) { report(error); }
}
// The first gate runs while niri is alive so infrastructure failures have pixels too.
try { checkLogs(); } catch (error) { failure ||= error; }
await retainFailure();
for (const child of [...children].reverse()) {
try { await stop(child); }
catch (error) { if (failure) report(error); else failure = error; }
}
// Retain the original complete-log gate, including diagnostics emitted on shutdown.
try { checkLogs(); } catch (error) { failure ||= error; }
await retainFailure();
if (failure) throw failure;
}
async function outer(replay) {
mkdirSync(join(project, '.tmp'), { recursive: true });
const evidence = mkdtempSync(join(project, '.tmp/ui-test-'));
const runtime = mkdtempSync(join(tmpdir(), 'nuguland-ui-runtime-'));
writeFileSync(join(runtime, 'passwd'), `test:x:${process.getuid()}:${process.getgid()}:test:/home/test:/bin/sh\n`);
const args = [
'--unshare-all', '--die-with-parent', '--new-session',
'--ro-bind', '/usr', '/usr', '--symlink', 'usr/bin', '/bin',
'--symlink', 'usr/lib', '/lib', '--symlink', 'usr/lib', '/lib64',
'--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp',
'--ro-bind', '/etc/fonts', '/etc/fonts',
'--ro-bind', '/etc/ld.so.cache', '/etc/ld.so.cache',
'--ro-bind', '/etc/machine-id', '/etc/machine-id',
'--ro-bind', join(runtime, 'passwd'), '/etc/passwd',
'--dir', '/home/test', '--dir', '/run/user',
'--overlay-src', project, '--tmp-overlay', '/project', '--bind', evidence, '/evidence',
'--bind', runtime, '/run/user/1000',
'--chdir', '/project', '--clearenv',
'--setenv', 'PATH', '/usr/bin', '--setenv', 'HOME', '/home/test',
'--setenv', 'XDG_RUNTIME_DIR', '/run/user/1000',
'--setenv', 'XDG_CONFIG_HOME', '/home/test/.config',
'--setenv', 'XDG_CACHE_HOME', '/home/test/.cache',
'--setenv', 'XDG_DATA_HOME', '/home/test/.local/share',
'--setenv', 'XDG_STATE_HOME', '/home/test/.local/state',
'--setenv', 'QT_QPA_PLATFORM', 'wayland',
'--setenv', 'LIBGL_ALWAYS_SOFTWARE', '1',
'--setenv', 'NUGULAND_UI_SANDBOX', '1',
'dbus-run-session', '--', 'node', '/project/ui-test.mjs', '--inside', replay.fixture, ...(replay.size ? [replay.size] : [])
];
console.log(`evidence: ${evidence}`);
writeFileSync(join(evidence, 'replay.json'), JSON.stringify({ ...replay, phase: 'startup' }, null, 2));
writeFileSync(join(evidence, 'command.json'), JSON.stringify({ cwd: project, command: ['bwrap', ...args] }, null, 2));
const child = spawn('bwrap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
for (const stream of [child.stdout, child.stderr]) stream.on('data', data => {
appendFileSync(join(evidence, 'runner.log'), data);
process.stdout.write(data);
});
let interrupted = false;
const interrupt = () => {
if (interrupted) return;
interrupted = true;
void stopChild(child);
};
process.on('SIGINT', interrupt);
process.on('SIGTERM', interrupt);
const timeout = setTimeout(interrupt, 300_000);
try {
const code = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (code, signal) => resolve(signal ? -1 : code));
});
assert(!interrupted, `nested UI test interrupted; full logs: ${evidence}`);
assert.equal(code, 0, `nested UI test failed; full logs: ${evidence}`);
} catch (error) {
// An inner failure already owns its evidence; never replace it with bwrap's exit code.
if (!existsSync(join(evidence, 'failure.json'))) {
try {
const { phase } = JSON.parse(readFileSync(join(evidence, 'replay.json'), 'utf8'));
await captureUiFailure(error, {
evidence, fixture: replay.fixture, phase, snapshot: null,
screenshot: async () => { throw new Error('sandbox unavailable for capture'); }
});
} catch (captureError) { console.error(captureError.stack); }
}
throw error;
} finally {
clearTimeout(timeout);
process.off('SIGINT', interrupt);
process.off('SIGTERM', interrupt);
await stopChild(child);
rmSync(runtime, { recursive: true });
assert(!existsSync(runtime), 'owned runtime was not removed');
}
}
async function inside(replay) {
assert.equal(process.env.NUGULAND_UI_SANDBOX, '1', 'internal mode requires sandbox');
assert.equal(process.env.HOME, '/home/test');
assert.equal(process.env.WAYLAND_DISPLAY, undefined, 'parent Wayland connection must not be inherited');
assert.equal(process.env.DISPLAY, undefined, 'parent X11 connection must not be inherited');
assert(!existsSync('/run/dbus/system_bus_socket'), 'host system bus must be inaccessible');
assert(!existsSync('/home/oli'), 'host HOME must be inaccessible');
assert(process.env.DBUS_SESSION_BUS_ADDRESS, 'private session bus required');
const evidence = '/evidence';
const children = [];
const runtimeLogs = [];
let fixtureLog = '';
let phase = 'startup';
let latestSnapshot = null;
let fixtureProcess = null;
let expectedWriteFailure = null;
let snapshot = () => { throw new Error('fixture snapshot not ready'); };
let failureScreenshot = async () => { throw new Error('child compositor not ready'); };
const controls = replay.fixture === 'all' || replay.fixture === 'controls';
const refactor = !['controls', 'niri', taskSelection].includes(replay.fixture);
const tasks = replay.fixture === 'all' || replay.fixture === taskSelection;
const integration = replay.fixture === 'all' || replay.fixture === 'niri';
const nativeNiri = readFileSync('/project/Niri.qml', 'utf8');
const nativeResources = readFileSync('/project/Resources.qml', 'utf8');
const taskOriginals = new Map(taskOverlayFixtures.map(({ target }) => [target, readFileSync(`/project/${target}`, 'utf8')]));
const [outputWidth, outputHeight] = (replay.size || '1280x720').split('x').map(Number);
function prepareTasks() {
for (const { source, target } of taskOverlayFixtures) {
const text = readFileSync(`/project/${source}`, 'utf8');
assertPureService(text);
writeFileSync(`/project/${target}`, text);
}
writeFileSync(`/project/${taskEntryFixture.target}`, readFileSync(`/project/${taskEntryFixture.source}`, 'utf8').replace(taskEntryFixture.replace.from, taskEntryFixture.replace.to));
}
function enterPhase(name) {
phase = name;
writeFileSync(join(evidence, 'replay.json'), JSON.stringify({ ...replay, phase }, null, 2));
console.log(`phase: ${name} (fixture: ${replay.fixture})`);
}
// Compositor system-bus watchers use the private bus, never the host bus.
let env = { ...process.env, DBUS_SYSTEM_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS };
function run(command, args = []) {
const result = spawnSync(command, args, { env, encoding: 'utf8', timeout: 30_000 });
appendFileSync(join(evidence, 'commands.log'), `${JSON.stringify([command, ...args])}\n${result.stdout || ''}${result.stderr || ''}exit: ${result.status}\n`);
if (result.error) throw result.error;
assert.equal(result.status, 0, `${command}: ${result.stdout}${result.stderr}`);
return result.stdout.trim();
}
function start(command, args, name, childEnv = env) {
appendFileSync(join(evidence, 'commands.log'), `start: ${JSON.stringify([command, ...args])}\n`);
const child = spawn(command, args, { env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] });
const log = join(evidence, `${name}.log`);
writeFileSync(log, '');
runtimeLogs.push(log);
for (const stream of [child.stdout, child.stderr]) stream.on('data', data => appendFileSync(log, data));
child.on('error', error => { child.failure = error; });
children.push(child);
return child;
}
async function waitFor(label, predicate) {
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
for (const child of children) {
if (child.failure) throw child.failure;
assert.equal(child.exitCode, null, `child exited during ${label}; inspect logs`);
assert.equal(child.signalCode, null, `child signalled during ${label}; inspect logs`);
}
if (fixtureLog) assertCleanLog(readFileSync(fixtureLog, 'utf8'));
const value = predicate();
if (value) return value;
await delay(50);
}
throw new Error(`timeout: ${label}`);
}
let failure = null;
try {
enterPhase('startup');
// Drop a host-generated symlink only in the disposable overlay.
rmSync('/project/.qmlls.ini', { force: true });
writeFileSync('/project/.qmlls.ini', '');
writeFileSync(join(evidence, 'versions.txt'), [run('weston', ['--version']), run('niri', ['--version']), run('qs', ['--version']), run('wdotool', ['--version']), run('/usr/lib/qt6/bin/qmllint', ['--version'])].join('\n') + '\n');
const runtime = process.env.XDG_RUNTIME_DIR;
const busFlags = run('pkg-config', ['--cflags', '--libs', 'libsystemd']).split(/\s+/);
run('cc', ['-std=c17', '-Wall', '-Wextra', '-Werror', '/project/test-fixtures/ui/session-bus.c', ...busFlags, '-o', '/tmp/nuguland-session-bus']);
start('/tmp/nuguland-session-bus', [], 'session-bus');
await waitFor('private login/locale fixtures', () => readFileSync(join(evidence, 'session-bus.log'), 'utf8').includes('session fixtures ready'));
const westonSocket = join(runtime, 'weston-headless');
const westonModules = [];
if (integration) {
const flags = run('pkg-config', ['--cflags', '--libs', 'libweston-15', 'wayland-server', 'xkbcommon']).split(/\s+/);
run('cc', ['-std=gnu17', '-Wall', '-Wextra', '-Werror', '-shared', '-fPIC', '/project/test-fixtures/ui/niri-integration-keyboard.c', ...flags, '-o', '/tmp/niri-integration-keyboard.so']);
run('mkfifo', [join(runtime, 'niri-integration-keyboard.fifo')]);
westonModules.push('--modules=/tmp/niri-integration-keyboard.so');
}
start('weston', ['--backend=headless', '--renderer=pixman', '--shell=kiosk', '--socket=weston-headless', `--width=${outputWidth}`, `--height=${outputHeight}`, '--idle-time=0', '--no-config', '--fake-seat', ...westonModules], 'weston');
await waitFor('headless Weston socket', () => existsSync(westonSocket) && lstatSync(westonSocket).isSocket());
env.WAYLAND_DISPLAY = westonSocket;
run('niri', ['validate', '--config', '/project/test-fixtures/ui/niri.kdl']);
const compositor = start('niri', ['--config', '/project/test-fixtures/ui/niri.kdl'], 'niri');
const endpoints = await waitFor('nested compositor sockets', () => {
const files = readdirSync(runtime);
const display = files.find(name => /^wayland-\d+$/.test(name));
const socket = files.find(name => /^niri\..*\.sock$/.test(name));
return display && socket ? [join(runtime, display), join(runtime, socket)] : null;
});
env = { ...env, ...childEnvironment(runtime, ...endpoints), DBUS_SYSTEM_BUS_ADDRESS: env.DBUS_SYSTEM_BUS_ADDRESS };
await waitFor('Niri consumed private login/locale properties', () => {
const log = readFileSync(join(evidence, 'session-bus.log'), 'utf8');
return ['org.freedesktop.login1.Manager.LidClosed', ...['X11Model', 'X11Layout', 'X11Variant', 'X11Options'].map(name => `org.freedesktop.locale1.${name}`)].every(name => log.includes(`read ${name}\n`));
});
assert.equal(run('busctl', ['--address', env.DBUS_SESSION_BUS_ADDRESS, 'get-property', 'org.freedesktop.login1', '/org/freedesktop/login1', 'org.freedesktop.login1.Manager', 'LidClosed']), 'b false');
assert.equal(run('busctl', ['--address', env.DBUS_SESSION_BUS_ADDRESS, 'get-property', 'org.freedesktop.locale1', '/org/freedesktop/locale1', 'org.freedesktop.locale1', 'X11Layout']), 's "us"');
assert.notEqual(env.WAYLAND_DISPLAY, westonSocket, 'input must target niri, not Weston');
async function screenshot(name) {
// Never change the failing UI to make evidence look safe or successful.
// Password-mode pixels are already masked; revealed input refuses capture.
await delay(300);
assert.equal(compositor.exitCode, null, 'child compositor exited before capture');
assert.equal(compositor.signalCode, null, 'child compositor signalled before capture');
childEnvironment(runtime, ...endpoints);
if (fixtureProcess?.pid && fixtureProcess.exitCode === null && fixtureProcess.signalCode === null) assertSafeCapture(snapshot());
const path = join(evidence, `${name}.png`);
run('niri', ['msg', 'action', 'screenshot-screen', '--show-pointer', 'false', '--path', path]);
// Failure capture must not run the fixture-log gate that caused the failure.
const deadline = Date.now() + 3000;
while (Date.now() < deadline) {
if (existsSync(path) && lstatSync(path).size > 0) return path;
await delay(50);
}
throw new Error(`timeout: screenshot ${name}`);
}
failureScreenshot = screenshot;
writeFileSync(join(evidence, 'outputs.json'), run('niri', ['msg', '--json', 'outputs']));
start('wdotool', ['--backend', 'wlr-protocols', 'prime'], 'input');
await waitFor('virtual input ready', () => readFileSync(join(evidence, 'input.log'), 'utf8').includes('ready'));
if (refactor) {
for (const name of ['CalDav', 'Resources', 'Privilege', 'Noko', 'Niri', 'Audio', 'Network', 'Vault']) {
const source = `/project/test-fixtures/ui/services/${name}.qml`;
assertPureService(readFileSync(source, 'utf8'));
copyFileSync(source, `/project/${name}.qml`);
}
// Keep the scanner root at /project, as for the original shared fixture.
writeFileSync('/project/refactor-ui-test.qml', readFileSync('/project/test-fixtures/ui/refactor.qml', 'utf8').replace('import "../.." as N', 'import "." as N'));
}
let integrationSetup;
async function prepareIntegration() {
integrationSetup = await prepareNiriIntegration({ run, start, waitFor, runtime, nativeNiri, nativeSocket: endpoints[1], assertPureService });
childEnvironment(runtime, endpoints[0], integrationSetup.socket);
}
if (replay.fixture === 'niri') await prepareIntegration();
if (replay.fixture === taskSelection) prepareTasks();
let fixture = controls ? '/project/ui-test.qml' : refactor ? '/project/refactor-ui-test.qml' : tasks ? '/project/tasks-ui-test.qml' : '/project/niri-integration-ui-test.qml';
const firstLog = controls ? 'quickshell' : refactor ? 'refactor' : tasks ? 'tasks' : 'niri-integration';
fixtureProcess = start('qs', ['--no-color', '-p', fixture], firstLog);
fixtureLog = join(evidence, `${firstLog}.log`);
await waitFor('quickshell loaded', () => {
const log = readFileSync(fixtureLog, 'utf8');
assertCleanLog(log);
return log.includes('Configuration Loaded');
});
const qmllsConfig = await waitFor('generated qmlls configuration', () => {
const config = readFileSync('/project/.qmlls.ini', 'utf8');
return config.includes('importPaths=') && config.includes('buildDir=') ? config : null;
});
writeFileSync(join(evidence, 'qmlls.ini'), qmllsConfig);
console.log('pass: Quickshell generated its upstream-recommended qmlls configuration');
const ipc = (...args) => run('qs', ['ipc', '-p', fixture, 'call', 'uiTest', ...args]);
snapshot = () => {
const text = ipc('snapshot');
assert(!text.includes('fixture-only'), 'snapshot exposed synthetic input');
const state = JSON.parse(text);
latestSnapshot = state;
return state;
};
const input = (...args) => run('wdotool', ['--backend', 'wlr-protocols', ...args]);
const key = key => input('key', key);
const click = rect => {
input('mousemove', String(Math.round(rect.x + rect.width / 2)), String(Math.round(rect.y + rect.height / 2)));
input('click', '1');
};
async function check(label, predicate) {
const state = await waitFor(label, () => { const state = snapshot(); return predicate(state) ? state : null; });
appendFileSync(join(evidence, 'assertions.jsonl'), JSON.stringify({ label, state }) + '\n');
console.log(`pass: ${label}`);
return state;
}
let state;
if (controls) {
enterPhase('controls');
state = await check('initial bindings and focus', s => !s.muted && !s.checked && s.volume === 0.3 && s.sliderValue === 0.3 && s.focus === 'mute');
assert.equal(state.width, 1280, 'headless output width');
assert.equal(state.height, 720, 'headless output height');
for (const name of ['surface', 'mute', 'slider', 'field']) {
const rect = state[name];
assert(rect.x >= 0 && rect.y >= 0 && rect.width > 0 && rect.height > 0 && rect.x + rect.width <= state.width && rect.y + rect.height <= state.height, `${name} outside mapped surface`);
}
await screenshot('initial');
click(state.mute);
state = await check('pointer toggles owner and checked state', s => s.muted && s.checked && s.writes === 1);
key('space');
await check('space activates focused button', s => !s.muted && !s.checked && s.writes === 2);
key('Return');
await check('enter activates focused button', s => s.muted && s.checked && s.writes === 3);
key('Tab');
await check('tab reaches slider', s => s.focus === 'slider');
key('Right');
await check('arrow updates slider owner', s => Math.abs(s.volume - 0.35) < 0.001 && s.sliderValue === s.volume);
click({ ...state.slider, width: state.slider.width * 1.5 });
await check('pointer updates slider owner', s => Math.abs(s.volume - 0.75) < 0.02 && s.sliderValue === s.volume);
key('Left');
await check('left arrow decreases slider owner', s => Math.abs(s.volume - 0.7) < 0.02 && s.sliderValue === s.volume);
key('Tab');
await check('tab reaches text input', s => s.focus === 'field');
input('type', 'headphones');
await check('text input updates fixture query', s => s.queryIsHeadphones && s.queryMatchesInput);
key('Return');
await check('enter accepts text field', s => s.accepted === 1);
key('Escape');
await check('escape cancels text field', s => s.cancelled === 1);
key('shift+Tab');
await check('reverse traversal reaches slider', s => s.focus === 'slider');
key('shift+Tab');
state = await check('reverse traversal reaches button', s => s.focus === 'mute');
const writes = state.writes;
ipc('external', 'true', '0.6', 'false');
state = await check('external state updates controls without writeback', s => s.muted && s.checked && s.volume === 0.6 && s.sliderValue === 0.6 && !s.enabled && s.writes === writes);
click(state.mute);
key('space');
click(state.slider);
key('Right');
click(state.field);
input('type', ' test');
await check('disabled controls reject input before the next field edit', s => s.queryIsEdited && s.queryMatchesInput && s.writes === writes);
input('mousemove', String(state.width - 10), String(state.height - 10));
await check('pointer leaves button tooltip', s => !s.tooltipVisible);
await screenshot('external-disabled');
for (const value of [0, 0.25, 0.75, 1]) {
ipc('external', 'false', String(value), 'true');
state = await check(`external slider value ${value} is visible`, s => s.sliderValue === value);
const before = state.writes;
click(state.sliderThumb);
state = await check(`pressing thumb at ${value} does not jump or write`, s => s.writes === before && s.volume === value && s.focus === 'slider');
}
input('mousemove', String(state.sliderThumb.x + state.sliderThumb.width / 2), String(state.sliderThumb.y + state.sliderThumb.height / 2));
input('mousedown', '1');
await check('thumb drag holds pointer grab', s => s.sliderPressed);
for (const value of [0.75, 0.25, 0]) {
input('mousemove', String(Math.round(state.slider.x + 8 + (state.slider.width - 16) * value)), String(state.slider.y + state.slider.height / 2));
await check(`continuous thumb drag reaches ${value}`, s => s.sliderPressed && Math.abs(s.volume - value) < 0.01);
}
input('mouseup', '1');
await check('thumb release ends drag', s => !s.sliderPressed);
click({ ...state.extras.card, width: 36 });
state = await check('interactive card primary activation is focused', s => s.extras.cardWrites === 1 && s.extras.cardFocused);
key('Tab');
await check('Tab reaches nested card action without focusing its parent', s => s.extras.nestedFocused && !s.extras.cardFocused);
key('space');
state = await check('nested action dispatches without parent activation', s => s.extras.nestedWrites === 1 && s.extras.cardWrites === 1);
key('Tab');
state = await check('Tab reaches named icon action', s => s.extras.iconFocused && s.extras.iconName === 'refresh devices' && s.extras.sliderName === 'output volume');
input('mousemove', String(state.width - 10), String(state.height - 10));
await check('keyboard focus exposes themed icon tooltip', s => s.extras.iconTooltipVisible);
await screenshot('keyboard-tooltip');
input('keydown', 'Return');
await delay(800);
state = await check('held Enter does not repeatedly dispatch', s => s.extras.iconWrites === 0);
input('keyup', 'Return');
await check('Enter release dispatches exactly once', s => s.extras.iconWrites === 1);
input('keydown', 'space');
await delay(800);
await check('held Space does not repeatedly dispatch', s => s.extras.iconWrites === 1);
input('keyup', 'space');
await check('Space release dispatches exactly once', s => s.extras.iconWrites === 2);
click(state.extras.disabled);
await check('unavailable control never dispatches', s => s.extras.disabledWrites === 0);
click({ ...state.extras.field, width: 4 });
await check('field padding focuses editor with its empty hint', s => s.extras.fieldFocused && s.extras.fieldHintVisible);
input('type', 'padding');
await check('typing after padding click edits normally', s => s.extras.fieldEdited && !s.extras.fieldHintVisible);
input('mousemove', ...[state.extras.longButton.x + 8, state.extras.longButton.y + 8].map(String));
await check('long useful tooltip opens on pointer hover', s => s.extras.longTooltipVisible);
await screenshot('wrapped-tooltip');
input('mousemove', String(state.width - 10), String(state.height - 10));
await check('leaving tooltip trigger dismisses it', s => !s.extras.longTooltipVisible);
await screenshot('controls-repaired');
assertCleanLog(readFileSync(join(evidence, 'quickshell.log'), 'utf8'));
console.log('shared-control UI assertions passed; screenshots require visual review');
}
if (controls && refactor) {
// Only the completed fixture stops. Weston, niri, input and the private bus stay alive.
await stopChild(fixtureProcess);
assertCleanLog(readFileSync(fixtureLog, 'utf8'));
children.splice(children.indexOf(fixtureProcess), 1);
latestSnapshot = null;
fixture = '/project/refactor-ui-test.qml';
fixtureProcess = start('qs', ['--no-color', '-p', fixture], 'refactor');
fixtureLog = join(evidence, 'refactor.log');
await waitFor('refactor fixture loaded', () => readFileSync(fixtureLog, 'utf8').includes('Configuration Loaded'));
}
const hasText = (rows, text) => rows.some(row => row.text === text);
const within = (rect, width, height) => rect && rect.width > 0 && rect.height > 0 && rect.x >= 0 && rect.y >= 0 && rect.x + rect.width <= width && rect.y + rect.height <= height;
function geometry(s, name) {
const vertical = s.orientation === 'vertical';
const width = name === 'clock' ? 760 : 780;
const height = name === 'clock' ? 580 : 600;
assert.deepEqual(s.popup.contentSize, { width, height });
assert.deepEqual(s.card, {
x: vertical ? 56 : Math.round((1280 - width) / 2) + 8,
y: vertical ? Math.round((720 - height) / 2) + 8 : 56,
width: width - 16, height: height - 16
});
assert(within(s.card, s.width, s.height));
assert(within(s.panel, s.width, s.height));
assert.equal(s.catalog.entries[name].component, name === 'clock' ? 'calendar-agenda' : 'system-resources');
}
function diagnostics() {
const text = run('qs', ['ipc', '-p', fixture, 'call', 'nuguland', 'diagnostics']);
assert(!text.includes('diagnostic-private-sentinel'), 'diagnostics exposed private fixture state');
const data = JSON.parse(text);
assert.equal(data.version, 1);
assert.deepEqual(data.services.niri, { available: true, ready: true, windowCount: 1, workspaceCount: 1 });
assert.deepEqual(data.services.vault, { status: 'locked', lockPending: false });
assert.deepEqual(data.displays, [{ x: 0, y: 0, width: 1280, height: 720, scale: 1 }]);
appendFileSync(join(evidence, 'diagnostics.jsonl'), text + '\n');
return data;
}
async function opened(name) {
const s = await check(`${name} open in mapped ${snapshot().orientation} geometry`, s => s.popup.name === name && s.popup.phase === 'open' && s.card && s.card.y === s.popup.popupCardFrame.currentRect.y);
geometry(s, name);
const diagnostic = diagnostics();
assert.equal(diagnostic.popup.name, name);
assert.equal(diagnostic.popup.phase, 'open');
assert.deepEqual(diagnostic.popup.card, s.card);
return s;
}
if (refactor) state = await check('refactor initially closed without resource polling', s => !s.popup.open && !s.resources.open);
if (replay.fixture === 'all' || replay.fixture === 'popouts') {
enterPhase('popouts');
for (const orientation of ['horizontal', 'vertical']) {
ipc('external', orientation);
state = snapshot();
click(state.buttons.clock);
state = await opened('clock');
if (orientation === 'horizontal') {
await screenshot('clock-setup');
click(state.calendar.url);
input('type', 'https://fixture.invalid/dav');
click(state.calendar.user);
input('type', 'fixture-user');
key('Tab');
input('type', 'fixture-only');
state = await check('calendar draft edits stay in the service until save', s => s.calendar.draftsEdited && s.calendar.saves === 0);
click(state.calendar.save);
await check('calendar save receives literal drafts and renders empty agenda', s => s.calendar.saves === 1 && s.calendar.savedDrafts && hasText(s.text, 'keine termine'));
await screenshot('clock-empty');
ipc('external', 'events');
await check('external calendar events render while open without another save', s => s.calendar.todayItems === 1 && hasText(s.text, 'fixture appointment') && s.calendar.saves === 1);
}
await screenshot(`clock-${orientation}`);
state = snapshot();
let opens = state.resources.opens;
click(state.buttons.system);
state = await opened('system');
assert.equal(state.resources.open, true);
assert.equal(state.resources.opens, opens + 1);
if (orientation === 'horizontal') {
assert(hasText(state.text, 'fixture-host'));
assert(hasText(state.text, 'fixture service failed'));
assert(hasText(state.text, 'fixture-worker · 25%'));
await check('process line is visible above current issues', s => {
const process = s.text.find(row => row.text === 'fixture-worker · 25%')?.rect;
const issues = s.text.find(row => row.text === 'needs attention')?.rect;
return within(process, s.width, s.height) && issues && process.y + process.height <= issues.y;
});
await screenshot('system-populated');
click(state.resources.action);
state = await check('resource issue action routes once with the issue id', s => s.resources.actions === 1 && s.resources.lastAction === 'fixture-unit');
key('Tab');
await check('Tab reaches the agent action after open', s => s.resources.agentFocused);
key('shift+Tab');
key('Tab');
key('Return');
state = await check('Enter dispatches one agent request and disables duplicates', s => s.resources.agentPending && s.resources.agentActions === 1 && s.resources.agentIssueId === 'fixture-unit');
click(state.resources.agent);
await check('pending pointer does not dispatch twice', s => s.resources.agentActions === 1);
ipc('external', 'agent-failure');
state = await check('missing executable leaves visible failure and retry', s => !s.resources.agentPending && hasText(s.text, 'pi or wezterm unavailable in session path'));
await screenshot('system-agent-unavailable');
click(state.resources.agent);
await check('pointer retries the agent request once', s => s.resources.agentActions === 2 && s.resources.agentPending);
ipc('external', 'agent-success');
state = await check('agent dispatch closes overlay and releases polling', s => !s.popup.open && !s.resources.open && s.popup.lastCloseReason === 'agent-dispatched');
click(state.buttons.system);
state = await opened('system');
opens++;
ipc('external', 'resource-incidents');
state = await check('battery and updates omit agent actions while crash retains one', s => !!s.resources.crashAgent && !s.resources.batteryAgent && !s.resources.updatesAgent && !!s.resources.updatesOpen);
await check('crash and oom history is neutral while current failures retain severity', s => {
const history = s.resources.issueRows.filter(row => row.historical);
const service = s.resources.issueRows.find(row => row.id === 'fixture-unit');
const battery = s.resources.issueRows.find(row => row.id === 'battery-health');
return hasText(s.text, 'incident history') && history.length === 2 && history.every(row => row.clock && !row.attention && !row.destructive && !row.highlighted) && service?.destructive && battery?.attention;
});
// The process line moves later rows below the viewport.
// Reach them through real keyboard traversal and focus reveal.
for (let attempts = 0; attempts < 10 && !snapshot().resources.updatesFocused; attempts++) key('Tab');
state = await check('keyboard reveals the update action below the process line', s => s.resources.updatesFocused && s.resources.updatesOpen.y + s.resources.updatesOpen.height <= s.panel.y + s.panel.height);
click(state.resources.updatesOpen);
await check('updates retain their existing open action', s => s.resources.actions === 2 && s.resources.lastAction === 'updates');
await screenshot('system-agent-eligibility');
// Tab skips non-actionable battery wear; then coverage and event open.
key('Tab');
key('Tab');
key('Tab');
key('space');
state = await check('Space targets one crash group with all three occurrences visible', s => s.resources.agentPending && s.resources.agentIssueId === s.resources.crashGroupId && hasText(s.text, '3 crashes · latest sigsegv · pid 123') && hasText(s.text, '4 recorded'));
const crashGroupId = state.resources.crashGroupId;
ipc('external', 'agent-failure');
await screenshot('system-grouped-crashes');
key('shift+Tab');
key('Tab');
key('Return');
state = await check('Enter retries the eligible historical agent action', s => s.resources.agentPending && s.resources.agentIssueId === crashGroupId);
ipc('external', 'resource-more-crashes');
state = await check('new occurrence updates count and latest time without changing group or pending state', s => s.resources.agentPending && s.resources.agentIssueId === crashGroupId && s.resources.crashGroupId === crashGroupId && hasText(s.text, '4 crashes · latest sigsegv · pid 200') && hasText(s.text, '5 recorded') && s.text.some(row => row.text.startsWith('latest · just now')));
const groupRequests = state.resources.agentActions;
click(state.resources.crashAgent);
await check('updated pending group still suppresses duplicate pointer dispatch', s => s.resources.agentActions === groupRequests);
ipc('external', 'agent-failure');
click(state.resources.crashOpen);
await check('group open routes the stable id to its latest event', s => s.resources.actions === 3 && s.resources.lastAction === crashGroupId);
click(state.resources.crashAgent);
await check('pointer retries the updated crash group', s => s.resources.agentActions === groupRequests + 1 && s.resources.agentIssueId === crashGroupId);
ipc('external', 'agent-failure');
await screenshot('system-incidents');
key('Tab');
key('Tab');
await screenshot('system-long-incident');
ipc('external', 'resource-history-unavailable');
await check('incomplete history retains recorded incidents', s => !!s.resources.crashAgent && hasText(s.text, 'incident history unavailable or incomplete'));
ipc('external', 'empty-resources');
await check('external empty issues and processes render all-clear', s => hasText(s.text, 'disk · memory · battery · updates ok') && !hasText(s.text, 'fixture service failed') && !hasText(s.text, 'fixture-worker · 25%') && !s.resources.action && s.resources.actions === 3);
}
await screenshot(`system-empty-${orientation}`);
click(state.buttons.clock);
state = await opened('clock');
assert.equal(state.resources.open, false, 'switching away must release resource gate');
assert.equal(state.resources.opens, state.resources.closes);
click(state.buttons.system);
state = await opened('system');
assert.equal(state.resources.opens, opens + 2);
key('Escape');
state = await check('Escape closes popup and releases resource lifecycle gate', s => !s.popup.open && !s.resources.open && s.popup.lastCloseReason === 'escape-key');
assert.equal(state.resources.opens, state.resources.closes);
click(state.buttons.system);
state = await opened('system');
assert.equal(state.resources.opens, opens + 3);
click({ x: 1200, y: 680, width: 10, height: 10 });
state = await check('outside pointer closes reopened system and releases gate', s => !s.popup.open && !s.resources.open && s.popup.lastCloseReason === 'outside');
assert.equal(state.resources.opens, state.resources.closes);
const refreshes = state.calendar.refreshes;
click(state.buttons.clock);
state = await opened('clock');
assert.equal(state.calendar.refreshes, refreshes + 1);
assert(hasText(state.text, 'fixture appointment'));
click(state.buttons.clock);
await check('bar pointer closes reopened clock', s => !s.popup.open && s.popup.lastCloseReason === 'bar-toggle');
}
}
if (replay.fixture === 'all' || replay.fixture === 'privilege') {
enterPhase('privilege');
ipc('external', 'privilege');
state = await check('privilege overlay maps and focuses literal input', s => s.auth.visible && s.auth.active && s.auth.inputFocused);
assert(within(state.auth.card, state.width, state.height));
assert(state.auth.shake.height > 0);
assert.equal(state.auth.shake.height, state.auth.shakeContentHeight);
assert.equal(state.auth.shake.width, state.auth.card.width - 44);
await screenshot('privilege-initial');
key('Tab');
await check('Tab reaches password reveal without focus theft', s => s.auth.revealFocused && !s.auth.inputFocused);
key('Tab');
await check('Tab reaches authentication cancel', s => s.auth.cancelFocused);
key('Tab');
await check('Tab reaches authentication authorize', s => s.auth.authorizeFocused);
key('Tab');
await check('authentication traversal wraps to input', s => s.auth.inputFocused);
key('shift+Tab');
await check('reverse Tab reaches authorize', s => s.auth.authorizeFocused);
key('shift+Tab');
await check('reverse Tab reaches cancel', s => s.auth.cancelFocused);
key('shift+Tab');
await check('reverse Tab reaches reveal', s => s.auth.revealFocused);
key('shift+Tab');
await check('reverse Tab returns to input', s => s.auth.inputFocused);
click(state.auth.input);
input('type', 'fixture-only');
await check('privilege input is present and masked without serializing its text', s => s.auth.hasInput && s.auth.inputMasked);
await screenshot('privilege-masked-input');
key('Return');
state = await check('privilege Enter routes literal submit and renders rejection', s => s.auth.submissions === 1 && s.auth.receivedLiteral && s.auth.active && hasText(s.auth.text, 'password rejected') && hasText(s.auth.text, 'fixture input'));
const failureCard = state.auth.card;
const failureHeight = state.auth.shake.height;
for (let i = 0; i < 4; i++) {
state = await check(`failure shake keeps supplied frame size ${i}`, s => s.auth.shake.height === s.auth.shakeContentHeight && s.auth.shake.height === failureHeight);
assert.deepEqual(state.auth.card, failureCard);
assert.equal(state.auth.shake.width, failureCard.width - 44);
await delay(60);
}
await screenshot('privilege-rejected');
click(state.auth.reveal);
state = await check('pointer reveal toggles without stealing input focus', s => s.auth.revealed && s.auth.revealFocused && !s.auth.inputFocused && hasText(s.auth.text, 'fixture input'));
// Reveal is captured with an empty field, never even a synthetic secret.
await screenshot('privilege-reveal-empty');
click(state.auth.reveal);
await check('pointer hides input again', s => !s.auth.revealed);
click(state.auth.input);
input('type', 'fixture-only');
key('Return');
state = await check('second Enter renders fixture authorization success', s => s.auth.submissions === 2 && s.auth.receivedLiteral && s.auth.confirming && !s.auth.active && hasText(s.auth.text, 'authorized'));
await screenshot('privilege-authorized');
ipc('external', 'privilege');
state = await check('new fixture request reacquires focus', s => s.auth.active && s.auth.inputFocused && !s.auth.confirming);
click(state.auth.cancel);
await check('pointer cancel dismisses privilege overlay', s => !s.auth.visible && s.auth.cancellations === 1);
ipc('external', 'privilege');
state = await check('privilege reopens after pointer cancellation', s => s.auth.visible && s.auth.inputFocused);
click(state.auth.input);
input('type', 'fixture-only');
key('Escape');
await check('Escape cancels privilege without submitting', s => !s.auth.visible && s.auth.cancellations === 2 && s.auth.submissions === 2);
}
if (replay.fixture === 'all' || replay.fixture === 'noko') {
enterPhase('noko');
ipc('external', 'horizontal');
for (const phase of ['capturing', 'transcribing', 'finished']) {
ipc('external', phase);
state = await check(`Noko renders ${phase}`, s => s.noko.visible && s.noko.phase === phase && hasText(s.noko.text, phase));
assert(within(state.noko.tab, state.width, state.height));
assert.equal(state.noko.tab.y, 66);
assert.equal(state.noko.tab.x, Math.round((state.width - state.noko.tab.width) / 2));
if (phase !== 'finished') await check(`Noko ${phase} animation ticks`, s => s.noko.tick > state.noko.tick);
await screenshot(`noko-${phase}`);
}
const finishedTick = state.noko.tick;
await delay(180);
assert.equal(snapshot().noko.tick, finishedTick);
ipc('external', 'vertical');
state = await check('Noko geometry reacts to vertical bar', s => s.noko.tab.y === 18 && s.noko.tab.x === Math.round(48 + (s.width - 48 - s.noko.tab.width) / 2));
await screenshot('noko-vertical');
ipc('external', 'thicker-bar');
await check('Noko geometry reacts to bar thickness without remount', s => s.noko.tab.x === Math.round(64 + (s.width - 64 - s.noko.tab.width) / 2));
await screenshot('noko-thicker-bar');
ipc('external', 'idle');
await check('Noko idle removes overlay', s => !s.noko.visible && s.noko.phase === '');
}
if (refactor) {
const diagnostic = diagnostics();
const transitions = await waitFor('transition log reaches diagnostic operation', () => {
const log = readFileSync(fixtureLog, 'utf8');
const events = log.split('\n').filter(line => line.includes('"kind":"popup-transition"')).map(line => JSON.parse(line.slice(line.indexOf('{'))));
return diagnostic.popup.operationId === 0 || events.at(-1)?.operationId === diagnostic.popup.operationId ? events : null;
});
assert(!JSON.stringify(transitions).includes('diagnostic-private-sentinel'));
if (replay.fixture === 'all' || replay.fixture === 'popouts') {
assert.deepEqual(transitions.slice(0, 4).map(({ operationId, event }) => [operationId, event]), [[1, 'opening'], [1, 'opened'], [2, 'switched'], [3, 'closed']]);
assert.equal(transitions.at(-1).event, 'closed');
assert.equal(transitions.at(-1).operationId, diagnostic.popup.operationId);
for (let i = 1; i < transitions.length; i++) {
const event = transitions[i];
const previous = transitions[i - 1];
assert.equal(event.operationId, previous.operationId + (event.event === 'opened' ? 0 : 1));
if (event.event === 'opened') assert.equal(previous.event, 'opening');
}
console.log('pass: redacted diagnostics correlate with runtime popup transitions');
} else assert.equal(transitions.length, 0);
console.log('refactor UI assertions passed; screenshots require visual review');
}
if (['all', 'popouts', 'system'].includes(replay.fixture)) {
await stopChild(fixtureProcess);
assertCleanLog(readFileSync(fixtureLog, 'utf8'));
children.splice(children.indexOf(fixtureProcess), 1);
latestSnapshot = null;
enterPhase('system-boundaries');
await runResourceProbe({original: nativeResources, env, start, stop: stopChild, waitFor, ipc, screenshot, snapshot,
setFixture(path, log, child) { fixture = path; fixtureLog = log; fixtureProcess = child; }
});
}
if (tasks) {
if (refactor) {
await stopChild(fixtureProcess);
assertCleanLog(readFileSync(fixtureLog, 'utf8'));
children.splice(children.indexOf(fixtureProcess), 1);
fixtureLog = '';
latestSnapshot = null;
prepareTasks();
fixture = '/project/tasks-ui-test.qml';
fixtureProcess = start('qs', ['--no-color', '-p', fixture], 'tasks');
fixtureLog = join(evidence, 'tasks.log');
await waitFor('task fixture loaded', () => readFileSync(fixtureLog, 'utf8').includes('Configuration Loaded'));
}
enterPhase(taskSelection);
await runTasks({ ipc, snapshot, input, key, click, check, screenshot, run, delay });
}
if (integration) {
if (refactor) {
await stopChild(fixtureProcess);
assertCleanLog(readFileSync(fixtureLog, 'utf8'));
children.splice(children.indexOf(fixtureProcess), 1);
fixtureLog = '';
latestSnapshot = null;
for (const [target, text] of taskOriginals)
writeFileSync(`/project/${target}`, text);
await prepareIntegration();
fixture = '/project/niri-integration-ui-test.qml';
fixtureProcess = start('qs', ['--no-color', '-p', fixture], 'niri-integration');
fixtureLog = join(evidence, 'niri-integration.log');
await waitFor('niri integration fixture loaded', () => readFileSync(fixtureLog, 'utf8').includes('Configuration Loaded'));
}
enterPhase('niri');
expectedWriteFailure = await runNiriIntegration({
run, start, check, snapshot, ipc, screenshot, click, input, key, waitFor, evidence, runtime,
modeFile: integrationSetup.modeFile,
compositorKey: async name => {
childEnvironment(runtime, ...endpoints);
assert.equal(compositor.exitCode, null);
assert.equal(compositor.signalCode, null);
const keys = { Escape: [1], Return: [28], 'ctrl+c': [29, 46] }[name];
assert(keys, 'unsupported nested compositor key');
for (const [key, pressed] of [...keys.map(key => [key, 1]), ...keys.toReversed().map(key => [key, 0])]) {
const command = Buffer.alloc(12);
[compositor.pid, key, pressed].forEach((value, index) => command.writeUInt32LE(value, index * 4));
writeFileSync(join(runtime, 'niri-integration-keyboard.fifo'), command);
appendFileSync(join(evidence, 'input.log'), `nested compositor ${compositor.pid}: key ${key} state ${pressed}\n`);
await delay(50);
}
},
stop: async child => {
await stopChild(child);
children.splice(children.indexOf(child), 1);
}
});
}
enterPhase('runtime-diagnostics');
} catch (error) {
failure = error;
} finally {
await finishUiRun(failure, {
children,
checkLogs: () => {
let diagnostics;
for (const log of runtimeLogs) {
const text = readFileSync(log, 'utf8');
if (log === join(evidence, 'niri.log')) diagnostics = assertNiriLog(text, expectedWriteFailure);
else assertCleanLog(text);
}
writeFileSync(join(evidence, 'runtime-diagnostics.json'), JSON.stringify({ ...diagnostics, expectedWriteFailure }, null, 2));
},
capture: error => captureUiFailure(error, {
evidence, fixture: replay.fixture, phase, snapshot: latestSnapshot, screenshot: failureScreenshot
})
});
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
if (process.argv[2] === '--inside') await inside(replayRequest(process.argv.slice(3)));
else await outer(replayRequest(process.argv.slice(2)));
} catch (error) {
console.error(error.stack);
process.exitCode = 1;
}
}