File size: 11,555 Bytes
39371ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 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));
}
|