MiniCPM5-2B-WebGPU-Pi-HTTP / app /tests /local-folder-browser.mjs
Mike0021's picture
Add optional local-folder workspace with staged review and apply
e7c75bb verified
Raw History Blame Contribute Delete
7.65 kB
import { chromium, webkit } from 'playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
const url = process.argv[2] || 'http://127.0.0.1:4193/';
const label = process.argv[3] || 'local-folder-browser';
const result = { url, date: new Date().toISOString(), checks: [], errors: [], picker: 'Test supplies a real OPFS directory handle; native OS picker covered separately.' };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
const page = await context.newPage();
page.on('pageerror', e => result.errors.push(e.message));
const idle = () => page.waitForFunction(() => window.browserPi && !window.browserPi.busy);
const rpc = (action, args) => page.evaluate(({ action, args }) => window.browserPi.rpc(action, args), { action, args });
const disk = name => page.evaluate(async name => (await (await window.testFolder.getFileHandle(name)).getFile()).text(), name);
async function open() { await page.goto(url); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready); await idle(); }
try {
await open();
const demo = await rpc('snapshot');
await page.evaluate(async () => {
const root = await navigator.storage.getDirectory();
window.testFolder = await root.getDirectoryHandle('project-fixture', { create: true });
const write = async (name, text) => { const s = await (await window.testFolder.getFileHandle(name, { create: true })).createWritable(); await s.write(text); await s.close(); };
await write('README.md', '# Local fixture\n'); await write('count.txt', '1\n');
const modules = await window.testFolder.getDirectoryHandle('node_modules', { create: true });
const ignored = await (await modules.getFileHandle('ignored.js', { create: true })).createWritable(); await ignored.write('ignore'); await ignored.close();
window.pickerCalls = 0;
window.showDirectoryPicker = async () => { window.pickerCalls++; window.pickerGesture = navigator.userActivation.isActive; return window.testFolder; };
});
await page.locator('#open-folder').click();
await page.waitForFunction(() => document.querySelector('#editor').value === '# Local fixture\n'); await idle();
assert.equal(await page.evaluate(() => window.pickerGesture), true);
assert.equal((await rpc('snapshot')).entries, null);
assert.equal(await page.locator('#files').innerText(), 'README.md\ncount.txt');
result.checks.push('Real directory handle crosses worker boundary; picker called with user activation; existing editor/tree reused; excluded directories omitted');
await page.locator('#editor').fill('# Edited local fixture\n'); await page.locator('#save').click(); await idle();
assert.equal(await disk('README.md'), '# Local fixture\n');
await page.locator('#review-changes').click();
await page.locator('#local-review').waitFor({ state: 'visible' });
assert.match(await page.locator('#review-files').innerText(), /\+# Edited local fixture/);
await page.screenshot({ path: `../validation/${label}-review.png` });
await page.locator('#review-apply').click(); await page.locator('#local-review').waitFor({ state: 'hidden' }); await idle();
assert.equal(await disk('README.md'), '# Edited local fixture\n');
result.checks.push('Editor changes are staged; readable diff reviewed; Apply writes through the browser API');
const shell = await rpc('shell', { command: "printf 'new file\\n' > added.txt; printf '2\\n' > count.txt" });
assert.equal(shell.exitCode, 0, shell.stderr); await idle();
assert.equal(await disk('count.txt'), '1\n');
await page.locator('#disconnect-folder').click(); await page.locator('#open-folder').waitFor({ state: 'visible' }); await idle();
assert.deepEqual((await rpc('snapshot')).files, demo.files);
assert.deepEqual((await rpc('snapshot')).messages, demo.messages);
await page.reload(); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready);
await page.locator('#reconnect-folder').click(); await page.locator('#local-folder-bar').waitFor({ state: 'visible' }); await idle();
const staged = await rpc('local_changes'); assert.equal(staged.length, 2);
await page.evaluate(async () => { window.testFolder = await (await navigator.storage.getDirectory()).getDirectoryHandle('project-fixture'); });
assert.equal(await rpc('read', { path: '/workspace/added.txt' }), 'new file\n'); await idle();
result.checks.push('Back to demo restores original files/chat; reload + reconnect restores handle, staged edits and local chat from separate IndexedDB record');
await page.evaluate(async () => { const s = await (await window.testFolder.getFileHandle('count.txt')).createWritable(); await s.write('9\n'); await s.close(); });
await page.locator('#review-changes').click(); await page.locator('#review-apply').click();
await page.waitForFunction(() => document.querySelector('#review-error').textContent.includes('Conflict'));
assert.equal(await disk('count.txt'), '9\n');
assert.equal(await page.evaluate(async () => { try { await window.testFolder.getFileHandle('added.txt'); return true; } catch { return false; } }), false);
page.once('dialog', d => d.accept()); await page.locator('#review-discard').click();
await page.locator('#local-review').waitFor({ state: 'hidden' }); await idle();
assert.equal((await rpc('local_changes')).length, 0);
result.checks.push('External edit blocks the whole apply before any disk mutation; Discard leaves external contents intact');
for (const width of [1440, 390, 320]) {
await page.setViewportSize({ width, height: width === 1440 ? 1000 : 844 });
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
await page.locator('#local-folder-bar').scrollIntoViewIfNeeded();
await page.screenshot({ path: `../validation/${label}-${width}.png`, fullPage: true });
}
result.checks.push('Folder controls and editor fit 1440, 390 and 320px layouts');
await page.locator('#disconnect-folder').click(); await idle();
await page.evaluate(() => { window.showDirectoryPicker = async () => { throw new DOMException('Cancelled', 'AbortError'); }; });
await page.locator('#open-folder').click(); await idle();
assert.deepEqual((await rpc('snapshot')).files, demo.files);
result.checks.push('Cancelling picker preserves the demo workspace');
// True cross-origin embedding, not a replaced window.top property.
const embed = await context.newPage();
await context.grantPermissions(['local-network-access']);
const parent = url.startsWith('http:') ? 'http://localhost:4193/embed-fixture' : 'https://embed.example/';
await embed.route(parent, route => route.fulfill({ contentType: 'text/html', body: `<iframe src="${url}" style="width:100%;height:900px"></iframe>` }));
await embed.goto(parent);
const frame = embed.frameLocator('iframe'); await frame.locator('#open-folder').click();
assert.match(await frame.locator('#notice').innerText(), /new tab/); await frame.locator('#open-tab').waitFor({ state: 'visible' });
await embed.close(); result.checks.push('Cross-origin iframe offers direct app tab instead of a blocked native picker');
assert.deepEqual(result.errors, []);
result.passed = true;
} catch (e) { result.failure = e.stack; await page.screenshot({ path: `../validation/${label}-failure.png`, fullPage: true }); throw e; }
finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await browser.close(); console.log(JSON.stringify(result, null, 2)); }