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: `` })); 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)); }