// The page-level API the Vite plugin builds on: configure() defaults, the getKernel() // singleton, runtime.mount() and runtime.applyFiles(). tests/fixtures/api/index.html // exposes the package as window.nib and a second module instance as window.nibCopy. import { test, expect, type Page } from "@playwright/test"; import type * as Nib from "../src/host/index"; // @ts-expect-error untyped build script import { snapshot, encodeSnapshot } from "../scripts/snapshot.mjs"; declare global { interface Window { nib: typeof Nib; nibCopy: typeof Nib } } async function openApi(page: Page): Promise { const paths: string[] = []; page.context().on("request", (req) => { paths.push(new URL(req.url()).pathname); }); await page.goto("/tests/fixtures/api/index.html"); await expect(page.locator("#result")).toHaveText("ready"); return paths; } test("getKernel() hands every caller and every copy of the module the same runtime", async ({ page }) => { await openApi(page); const out = await page.evaluate(async () => { const { nib, nibCopy } = window; const first = nib.getKernel(); const same = [nib.getKernel() === first, nibCopy.getKernel() === first]; await first; // Options that locate the same files are the same options; others conflict. same.push(nib.getKernel({ base: "/dist/runtime" }) === first); let conflict = ""; try { await nib.getKernel({ base: "/__nib/0.0.0/", serviceWorker: "/nib-sw.js" }); } catch (e) { conflict = `${(e as Error).name} ${(e as Nib.NibError).code} ${String(e instanceof nib.NibError)}`; } // A closed runtime is forgotten: the next call boots a new one. await (await first).close(); const next = nib.getKernel(); const fresh = next !== first && (await next) !== (await first); return { same, conflict, fresh }; }); expect(out).toEqual({ same: [true, true, true], conflict: "KernelExistsError ERR_NIB_KERNEL_EXISTS true", fresh: true }); }); test("a getKernel() whose boot fails is forgotten, so the next call boots", async ({ page }) => { await openApi(page); const out = await page.evaluate(async () => { const { nib } = window; let failed = ""; try { await nib.getKernel({ serviceWorker: "/tests/fixtures/api/missing-sw.js" }); } catch (e) { failed = (e as Nib.NibError).code; } const runtime = await nib.getKernel(); return { failed, booted: typeof runtime.exec }; }); expect(out).toEqual({ failed: "ERR_NIB_SW_REGISTER", booted: "function" }); }); test("configure() sets defaults that boot() picks up from any copy of the module", async ({ page }) => { const paths = await openApi(page); const sw = await page.evaluate(async () => { window.nib.configure({ base: "/__nib/0.0.0/", serviceWorker: "/nib-sw.js" }); await window.nibCopy.boot(); return new URL(navigator.serviceWorker.controller!.scriptURL).pathname; }); expect(sw).toBe("/nib-sw.js"); expect(paths).toContain("/__nib/0.0.0/kernel.js"); expect(paths).not.toContain("/dist/runtime/kernel.js"); }); test("options passed to boot() win over configure(), field by field", async ({ page }) => { const paths = await openApi(page); const sw = await page.evaluate(async () => { window.nib.configure({ base: "/nowhere/", serviceWorker: "/nib-sw.js" }); await window.nib.boot({ base: "/__nib/0.0.0/" }); return new URL(navigator.serviceWorker.controller!.scriptURL).pathname; }); expect(sw).toBe("/nib-sw.js"); expect(paths).toContain("/__nib/0.0.0/kernel.js"); expect(paths.filter((p) => p.startsWith("/nowhere/"))).toEqual([]); }); /** Mount `app`, run the Express fixture's server and fetch its page. */ async function mountAndServe(page: Page, app: "url" | "URL" | "nibApp"): Promise<{ root: string; body: string }> { return await page.evaluate(async (app) => { const runtime = await window.nib.getKernel(); const src = "/dist/fixtures/express.snap"; const target = app === "url" ? src : app === "URL" ? new URL(src, location.href) : { src, root: "/app", entry: "server.cjs", files: 0, bytes: 0 }; const { root } = await runtime.mount(target); runtime.exec(`${root}/server.cjs`, { env: { NODE_ENV: "production" } }); const res = await fetch(await runtime.waitForPort(3000)); return { root, body: await res.text() }; }, app); } for (const kind of ["url", "URL", "nibApp"] as const) { test(`mount() from a ${kind} returns the app's root and the app runs`, async ({ page }) => { await openApi(page); const { root, body } = await mountAndServe(page, kind); expect(root).toBe("/app"); expect(body).toContain('

Hello from Express

'); }); } test("mount() of a buffer returns the deepest directory holding its files", async ({ page }) => { await openApi(page); const bytes = Array.from(encodeSnapshot(snapshot("tests/fixtures/snap-app", "/srv/demo")) as Uint8Array); const root = await page.evaluate(async (bytes) => { const runtime = await window.nib.getKernel(); return (await runtime.mount(new Uint8Array(bytes).buffer)).root; }, bytes); expect(root).toBe("/srv/demo"); }); test("mount() with replace: true prunes the root down to the snapshot's files, and only the root", async ({ page }) => { await openApi(page); const bytes = Array.from(encodeSnapshot(snapshot("tests/fixtures/snap-app", "/srv/demo")) as Uint8Array); const out = await page.evaluate(async (bytes) => { const runtime = await window.nib.getKernel(); const extra = { "/srv/demo/stale.txt": "gone", "/srv/demo/old/deep/x.txt": "gone", "/srv/demo/public/stale.css": "gone", "/srv/demo-sibling.txt": "kept", "/srv/kept.txt": "kept", }; const exists = (path: string) => runtime.fs.stat(path).then(() => true, () => false); const check = async () => Object.fromEntries(await Promise.all( [...Object.keys(extra), "/srv/demo/old", "/srv/demo/public/style.css", "/srv/demo/server.mjs"].map(async (p) => [p, await exists(p)]))); await runtime.applyFiles(extra); await runtime.mount(new Uint8Array(bytes).buffer); const seeded = await check(); await runtime.fs.mkdir("/srv/demo/empty"); const { root } = await runtime.mount(new Uint8Array(bytes).buffer, { replace: true }); const replaced = { ...(await check()), "/srv/demo/empty": await exists("/srv/demo/empty") }; let refused = ""; try { await runtime.mount({ src: "/dist/fixtures/express.snap", root: "/", files: 0, bytes: 0 }, { replace: true }); } catch (e) { refused = `${(e as Error).name}: ${(e as Error).message}`; } return { root, seeded, replaced, refused }; }, bytes); expect(out.root).toBe("/srv/demo"); // A plain mount writes over the tree and keeps what the snapshot lacks. expect(Object.values(out.seeded).every(Boolean)).toBe(true); expect(out.replaced).toEqual({ "/srv/demo/stale.txt": false, "/srv/demo/old/deep/x.txt": false, "/srv/demo/old": false, "/srv/demo/public/stale.css": false, "/srv/demo/public/style.css": true, "/srv/demo/server.mjs": true, "/srv/demo/empty": false, "/srv/demo-sibling.txt": true, "/srv/kept.txt": true, }); expect(out.refused).toBe('TypeError: mount() with replace: true needs an app root below "/"'); }); test("mount() of a missing snapshot rejects with ERR_NIB_MOUNT naming the URL and status", async ({ page }) => { await openApi(page); const err = await page.evaluate(async () => { const runtime = await window.nib.getKernel(); try { await runtime.mount("/dist/fixtures/missing.snap"); return "mounted"; } catch (e) { return `${(e as Nib.NibError).code}: ${(e as Error).message}`; } }); expect(err).toBe(`ERR_NIB_MOUNT: fetching the snapshot ${new URL("/dist/fixtures/missing.snap", page.url()).href} failed: 404 Not Found`); }); test("applyFiles() writes and deletes files, and a running guest's fs.watch sees it", async ({ page }) => { await openApi(page); const out = await page.evaluate(async () => { const runtime = await window.nib.getKernel(); runtime.writeTree({ "/app/w/gone.txt": "old", "/app/watch.cjs": [ "const fs = require('node:fs');", "const seen = new Set();", "const watcher = fs.watch('/app/w', (type, name) => {", " if (seen.size === 0) setTimeout(() => {", " watcher.close();", " console.log([...seen].sort().join(', '));", " console.log(fs.readFileSync('/app/w/new.txt', 'utf8'), fs.readFileSync('/app/w/deep/a/b.bin').toString('hex'), fs.existsSync('/app/w/gone.txt'));", " }, 300);", " seen.add(`${type} ${name}`);", "});", "console.log('watching');", ].join("\n"), }); const proc = runtime.exec("/app/watch.cjs"); const dec = new TextDecoder(); let stdout = ""; let stderr = ""; let watching!: () => void; const ready = new Promise((resolve) => { watching = resolve; }); const piped = Promise.all([ proc.stdout.pipeTo(new WritableStream({ write(c) { stdout += dec.decode(c); if (stdout.includes("watching\n")) watching(); } })), proc.stderr.pipeTo(new WritableStream({ write(c) { stderr += dec.decode(c); } })), ]); await ready; await runtime.applyFiles({ "/app/w/new.txt": "hello", "/app/w/deep/a/b.bin": new Uint8Array([0xca, 0xfe]), "/app/w/gone.txt": null, "/app/w/never-existed.txt": null, }); const hostRead = new TextDecoder().decode(await runtime.fs.readFile("/app/w/new.txt")); const code = await proc.exited; await piped; return { code, stdout, stderr, hostRead }; }); expect(out).toEqual({ code: 0, // Only the watched directory's own entries report (inotify), so deep/a/b.bin is "rename deep". stdout: "watching\nchange new.txt, rename deep, rename gone.txt, rename new.txt\nhello cafe false\n", stderr: "", hostRead: "hello", }); });