Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
11.6 kB
import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
const url = process.argv[2] ?? 'http://127.0.0.1:4173/';
const result = { url, fixture: 'Real app and workspace worker; controlled model load and prompt replies. GPU failure also tested without fixtures.', checks: [], errors: [] };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true, args: ['--disable-gpu', '--disable-software-rasterizer'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(15000);
page.on('pageerror', error => result.errors.push(error.message));
// Control only the model boundary; initialization, files and the terminal are real.
await page.addInitScript(() => {
const NativeWorker = Worker;
const fixture = window.loadFixture = { loads: [], prompts: [], cached: false };
window.Worker = class extends NativeWorker {
constructor(...args) { super(...args); this.isAgent = String(args[0]).includes('agent.worker'); if (this.isAgent) fixture.worker = this; }
postMessage(message, ...rest) {
if (!this.isAgent) return super.postMessage(message, ...rest);
const { id, action, args } = message;
if (action === 'cache_status' && fixture.cached) { fixture.emit({ type: 'reply', id, value: true }); return; }
if (action === 'load') {
fixture.loads.push(args); fixture.pending = id; fixture.stopped = false;
fixture.emit({ type: 'busy', action, cachedOnly: args.cachedOnly }); return;
}
if (action === 'stop' && fixture.pending) {
fixture.stopped = true; fixture.emit({ type: 'reply', id, value: true }); return;
}
if (action === 'prompt') {
fixture.prompts.push(args.text);
const message = { role: 'user', content: [{ type: 'text', text: args.text }], timestamp: Date.now() };
fixture.emit({ type: 'busy', action });
fixture.emit({ type: 'agent_event', event: { type: 'message_start', message } });
fixture.emit({ type: 'reply', id, value: { messages: [message] } });
fixture.emit({ type: 'idle' }); return;
}
return super.postMessage(message, ...rest);
}
};
fixture.emit = data => fixture.worker.dispatchEvent(new MessageEvent('message', { data }));
fixture.finish = (error, name = 'Error') => {
const id = fixture.pending; fixture.pending = undefined;
if (!error) fixture.emit({ type: 'loaded', device: { vendor: 'test fixture' } });
// Match the worker's error ordering: cleanup/idle before a rejected reply.
if (error) fixture.emit({ type: 'idle' });
fixture.emit({ type: 'reply', id, value: {}, error, errorName: name });
if (!error) fixture.emit({ type: 'idle' });
};
});
const ready = async () => { await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready); };
const emit = data => page.evaluate(data => window.loadFixture.emit(data), data);
const finish = (error, name) => page.evaluate(([error, name]) => window.loadFixture.finish(error, name), [error, name]);
const draft = text => page.waitForFunction(text => !window.browserPi.busy && !document.querySelector('#model-dialog').open && window.browserPi.terminal.text === text, text);
const send = async (text = '') => {
await page.locator('.xterm-helper-textarea').focus();
if (text) await page.keyboard.type(text);
await page.locator('#send').click();
await page.locator('#model-dialog[open]').waitFor();
};
const confirm = async () => {
await page.locator('#model-confirm').click();
await page.waitForFunction(() => window.browserPi.busy && !document.querySelector('#model-dialog').open);
};
try {
await page.goto(url); await ready();
const text = "What's our total sales revenue?";
await send(text); await page.locator('#model-cancel').click(); await draft(text);
assert.equal(await page.evaluate(() => window.loadFixture.loads.length), 0);
await send(); await confirm();
assert(await page.locator('#progress').isVisible());
assert.equal(await page.locator('#progress').getAttribute('value'), null);
assert.equal(await page.locator('#runtime-meter').getAttribute('data-state'), 'loading');
assert(await page.locator('#stop').isVisible());
for (const id of ['#send', '#new-chat', '#load']) assert(await page.locator(id).isDisabled());
await page.waitForFunction(text => window.browserPi.terminal.readScreen().includes(text) && window.browserPi.terminal.readScreen().includes('Queued'), text);
assert(await page.locator('.xterm-helper-textarea').evaluate(el => el === document.activeElement));
await page.evaluate(() => { document.querySelector('#model-confirm').click(); window.browserPi.terminal.submit(); document.querySelector('#send').click(); });
assert.equal(await page.evaluate(() => window.loadFixture.loads.length), 1);
assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
await emit({ type: 'load_progress', phase: 'download', loaded: 736000000, total: 1840000000 });
assert.equal(await page.locator('#progress').getAttribute('value'), '40');
assert.match(await page.locator('#load').getAttribute('title'), /Hugging Face/);
for (const [width, height] of [[1440,1000],[851,800],[850,800],[390,844],[320,568]]) {
await page.setViewportSize({ width, height });
await page.waitForTimeout(250); // Header capsule width transition.
assert(await page.evaluate(() => {
const header = document.querySelector('header').getBoundingClientRect();
return document.documentElement.scrollWidth <= innerWidth && [...document.querySelectorAll('#runtime-meter, .model-panel, #progress')].every(el => {
const r = el.getBoundingClientRect(); return r.left >= header.left && r.right <= header.right && r.top >= header.top && r.bottom <= header.bottom;
});
}), `Header overflow at ${width}`);
if ([1440,390,320].includes(width)) await page.screenshot({ path: `../validation/load-flow-${width}.png`, fullPage: true });
}
await emit({ type: 'load_progress', phase: 'compile' });
assert(await page.locator('#progress').isVisible()); assert.equal(await page.locator('#progress').getAttribute('value'), null);
await emit({ type: 'load_progress', phase: 'warmup' });
assert.equal(await page.locator('#load').textContent(), 'Almost ready…');
await page.locator('#stop').click();
assert(await page.evaluate(() => window.loadFixture.stopped && window.browserPi.busy));
assert(await page.locator('#send').isDisabled());
await finish('Stopped.', 'AbortError'); await draft(text);
assert(await page.locator('#progress').isHidden());
assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
result.checks.push('Confirmation closes immediately, queues the visible message, shows all header phases at five widths, rejects duplicate clicks; Stop waits for cleanup and restores the draft');
await page.setViewportSize({ width: 1440, height: 1000 });
await send(); await confirm(); await finish('Download interrupted. Please try again.');
await page.locator('#model-dialog[open]').waitFor();
assert.equal(await page.locator('#model-preview').textContent(), text);
assert.match(await page.locator('#model-error').textContent(), /Download interrupted/);
assert.equal(await page.locator('#model-confirm').textContent(), 'Try again');
assert(await page.locator('#progress').isHidden());
await page.keyboard.press('Escape'); await draft(text);
await send(); await confirm(); await finish('Temporary connection failure.');
await page.locator('#model-dialog[open]').waitFor();
await confirm();
await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type('A follow-up draft');
await page.locator('#files button').first().focus();
await finish();
await page.waitForFunction(() => window.loadFixture.prompts.length === 1 && !window.browserPi.busy);
assert.deepEqual(await page.evaluate(() => window.loadFixture.prompts), [text]);
assert.equal(await page.evaluate(() => window.browserPi.terminal.text), 'A follow-up draft');
assert(await page.locator('#model-dialog').isHidden());
assert(await page.locator('#progress').isHidden());
assert(await page.locator('#files button').first().evaluate(el => el === document.activeElement));
await page.waitForFunction(() => !window.browserPi.terminal.readScreen().includes('Queued'));
result.checks.push('Failure reopens retry with the original message; Escape keeps the draft; successful retry sends exactly once and retains a follow-up draft and the user’s current focus');
await page.reload(); await ready();
await page.evaluate(() => { window.loadFixture.cached = true; });
await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type('Cached message'); await page.locator('#send').click();
await page.waitForFunction(() => window.loadFixture.pending);
assert(await page.locator('#model-dialog').isHidden());
assert.equal(await page.evaluate(() => window.loadFixture.loads[0].cachedOnly), true);
await emit({ type: 'load_progress', phase: 'compile' });
assert(await page.locator('#progress').isVisible());
await page.keyboard.press('Escape'); await finish('Stopped.', 'AbortError'); await draft('Cached message');
result.checks.push('Cached restoration uses the same top bar without confirmation; Escape stops it without opening a retry dialog');
await page.reload(); await ready();
await page.locator('#load').click();
assert(await page.locator('#model-dialog').isHidden());
assert(await page.locator('#progress').isVisible());
await finish(); await page.waitForFunction(() => window.browserPi.loaded && !window.browserPi.busy);
assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
result.checks.push('Loading from the header still works without queuing or sending a message');
// Real unsupported-GPU load, with no worker interception or fake progress.
const actual = await browser.newPage({ viewport: { width: 390, height: 844 } });
actual.on('pageerror', error => result.errors.push(error.message));
await actual.goto(url); await actual.waitForFunction(() => window.browserPi?.terminal); await actual.evaluate(() => window.browserPi.ready);
await actual.locator('.xterm-helper-textarea').focus(); await actual.keyboard.type('Keep my message'); await actual.locator('#send').click();
await actual.locator('#model-confirm').click();
await actual.locator('#model-error').waitFor({ state: 'visible' });
assert.match(await actual.locator('#model-error').textContent(), /WebGPU/);
assert(await actual.locator('#progress').isHidden());
await actual.locator('#model-confirm').click(); await actual.locator('#model-error').waitFor({ state: 'visible' });
await actual.locator('#model-cancel').click();
await actual.waitForFunction(() => window.browserPi.terminal.text === 'Keep my message');
assert.equal((await actual.evaluate(() => window.browserPi.rpc('snapshot'))).messages.length, 0);
await actual.close(); result.checks.push('Actual unsupported-WebGPU load retries and cancels safely with no message sent');
assert.deepEqual(result.errors, []); result.passed = true;
} catch (error) {
result.failure = error.stack; await page.screenshot({ path: '../validation/load-flow-failure.png', fullPage: true }); throw error;
} finally {
await fs.writeFile('../validation/load-flow.json', JSON.stringify(result, null, 2));
await browser.close(); console.log(JSON.stringify(result));
}