Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284// Demo harness acceptance proofs: the REAL page UI, restyled as a VS Code// Explorer (activity bar | explorer sidebar | editor tabs + gutter | OUTPUT// panel | status bar). Every step is a click or a typing action through// Playwright locators (the same things a human does) — no page.evaluate on// runtime internals. The harness page exposes window.__runtime only as a debug// hook; these tests deliberately do not use it.//// Tree updates arrive via runtime.onFsChanged (the host coalesces ~25 ms) and guest// stdio arrives as stream chunks, so every assertion rides Playwright's// auto-retrying expect() with generous timeouts instead of fixed sleeps.//// Selector map for the VS Code behaviors (the brief's stable hooks):// #tree [data-path=…] tree rows (role=treeitem; dirs carry aria-expanded)// row .twisty folder expand/collapse chevron (▸ rotates to ▾)// #inline-row / #inline-input / #inline-ok create + rename (no window.prompt)// #confirm-row / #confirm-text / #confirm-yes recursive dir delete (no window.confirm)// #tabs .tab[data-path=…] editor tabs (role=tab; data-dirty="true|false",// .tab-dirty-dot is the ● marker)// #btn-save / #btn-editor-delete tab-bar actions, accessible names Save/Delete// #entry / #btn-run / #exit guest runner in the OUTPUT panel header// #status single outcome/notification element// #sb-boot / #sb-exit / #sb-selected status bar segments// Editor-library hooks (this slice):// #hl-wrap > #hl-mirror microlighter mirror behind #content for code// and markdown files (hidden for .txt/binary/// unknown); token colors via CSS Custom Highlight APIimport { test, expect, type Page } from "@playwright/test";
const tab = (page: Page, path: string) => page.locator(`#tabs .tab[data-path="${path}"]`);
async function openHarness(page: Page): Promise<void> { await page.goto("/demo/harness.html"); // The tree renders after boot + seed; the root row is the "ready" marker. await expect(page.locator('#tree [data-path="/app"]')).toBeVisible({ timeout: 20_000 }); await expect(page.locator("#sb-boot")).toContainText("VFS ready");}
// Right-click a tree row and pick a context-menu item (New File / New Folder /// Rename / Delete / Copy Path — ids ctx-*).async function ctxMenu(page: Page, path: string, item: string): Promise<void> { await page.locator(`#tree [data-path="${path}"]`).click({ button: "right" }); await expect(page.locator("#context-menu")).toBeVisible(); await page.locator(`#ctx-${item}`).click();}
test("boot: node_modules starts collapsed; its twisty reveals leftpad; scratch dir visible", async ({ page }) => { await openHarness(page); const tree = page.locator("#tree"); // Calm VS Code default: node_modules collapsed, the rest of the first level visible. await expect(tree.locator('[data-path="/app/node_modules"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/node_modules/leftpad"]')).toHaveCount(0); await expect(tree.locator('[data-path="/app/scratch"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/main.cjs"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/notes.md"]')).toBeVisible();
// Expanding the twisty reveals the packages; a nested folder still starts // collapsed until its own twisty is clicked. await tree.locator('[data-path="/app/node_modules"] .twisty').click(); await expect(tree.locator('[data-path="/app/node_modules/leftpad"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/node_modules/shouty"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/node_modules/leftpad/index.js"]')).toHaveCount(0); await tree.locator('[data-path="/app/node_modules/leftpad"] .twisty').click(); await expect(tree.locator('[data-path="/app/node_modules/leftpad/index.js"]')).toBeVisible();
// Expansion survives tree re-renders: creating a file fires onFsChanged → // re-render, and node_modules must still be open afterwards. await ctxMenu(page, "/app", "new-file"); await page.locator("#inline-input").fill("re-render-probe.txt"); await page.locator("#inline-ok").click(); await expect(tree.locator('[data-path="/app/re-render-probe.txt"]')).toBeVisible(); await expect(tree.locator('[data-path="/app/node_modules/leftpad/index.js"]')).toBeVisible();});
test("create: New File via context menu → type → Save (dirty dot clears) → guest echoes, exit 0", async ({ page }) => { await openHarness(page); await ctxMenu(page, "/app", "new-file"); await expect(page.locator("#inline-row")).toBeVisible(); await page.locator("#inline-input").fill("hello.txt"); await page.locator("#inline-ok").click();
// The created file auto-opens as the active editor tab. await expect(page.locator('#tree [data-path="/app/hello.txt"]')).toBeVisible(); await expect(tab(page, "/app/hello.txt")).toHaveAttribute("aria-selected", "true"); await page.locator("#content").fill("hi from the harness"); await expect(tab(page, "/app/hello.txt")).toHaveAttribute("data-dirty", "true"); await page.locator("#btn-save").click(); await expect(page.locator("#status")).toContainText("saved /app/hello.txt"); await expect(tab(page, "/app/hello.txt")).toHaveAttribute("data-dirty", "false");
// The guest reads what the page wrote — the cross-writer round-trip. await page.locator("#entry").fill("/app/echo.cjs"); await page.locator("#btn-run").click(); await expect(page.locator("#stdout")).toContainText("hi from the harness", { timeout: 15_000 }); await expect(page.locator("#exit")).toHaveText("0", { timeout: 15_000 }); await expect(page.locator("#stderr")).toHaveText("");});
test("guest→page: writer.cjs writeFileSync appears in the expanded scratch without a manual refresh", async ({ page }) => { await openHarness(page); await page.locator("#entry").fill("/app/writer.cjs"); await page.locator("#btn-run").click(); await expect(page.locator("#stdout")).toContainText("wrote /app/scratch/from-guest.txt", { timeout: 15_000 }); await expect(page.locator("#exit")).toHaveText("0", { timeout: 15_000 }); // No Refresh click anywhere in this test: the row must appear via onFsChanged. await expect(page.locator('#tree [data-path="/app/scratch/from-guest.txt"]')).toBeVisible({ timeout: 10_000 });});
test("edit + rename: notes.md dirty dot clears on save; hello.txt renames with content preserved", async ({ page }) => { await openHarness(page);
// Create hello.txt with content the rename must preserve. await ctxMenu(page, "/app", "new-file"); await page.locator("#inline-input").fill("hello.txt"); await page.locator("#inline-ok").click(); await expect(tab(page, "/app/hello.txt")).toHaveAttribute("aria-selected", "true"); await page.locator("#content").fill("hi from the harness"); await page.locator("#btn-save").click(); await expect(page.locator("#status")).toContainText("saved /app/hello.txt");
// Edit notes.md; the dirty dot shows until Save. await page.locator('#tree [data-path="/app/notes.md"]').click(); await expect(tab(page, "/app/notes.md")).toHaveAttribute("aria-selected", "true"); await expect(page.locator("#content")).toHaveValue("# scratch\n"); await expect(tab(page, "/app/notes.md")).toHaveAttribute("data-dirty", "false"); await page.locator("#content").fill("# scratch\nedited in the harness\n"); await expect(tab(page, "/app/notes.md")).toHaveAttribute("data-dirty", "true"); await expect(tab(page, "/app/notes.md").locator(".tab-dirty-dot")).toBeVisible(); await page.locator("#btn-save").click(); await expect(page.locator("#status")).toContainText("saved /app/notes.md"); await expect(tab(page, "/app/notes.md")).toHaveAttribute("data-dirty", "false"); await expect(tab(page, "/app/notes.md").locator(".tab-dirty-dot")).toBeHidden();
// Rename via the context menu's inline input, pre-filled with the current name. await page.locator('#tree [data-path="/app/hello.txt"]').click(); await ctxMenu(page, "/app/hello.txt", "rename"); await expect(page.locator("#inline-input")).toHaveValue("hello.txt"); await page.locator("#inline-input").fill("hello2.txt"); await page.locator("#inline-ok").click(); await expect(page.locator('#tree [data-path="/app/hello.txt"]')).toHaveCount(0); await expect(page.locator('#tree [data-path="/app/hello2.txt"]')).toBeVisible();
// Content preserved: open the renamed file and read it back. await page.locator('#tree [data-path="/app/hello2.txt"]').click(); await expect(page.locator("#content")).toHaveValue("hi from the harness");
// The notes.md edit survived its own tree refresh too. await page.locator('#tree [data-path="/app/notes.md"]').click(); await expect(page.locator("#content")).toHaveValue("# scratch\nedited in the harness\n");});
test("delete: file rm is immediate from the tab actions; dir delete goes through the inline recursive confirm", async ({ page }) => { await openHarness(page);
// Make /app/scratch/from-guest.txt exist by running the seeded writer guest. await page.locator("#entry").fill("/app/writer.cjs"); await page.locator("#btn-run").click(); await expect(page.locator('#tree [data-path="/app/scratch/from-guest.txt"]')).toBeVisible({ timeout: 10_000 });
// File delete: the tab-bar Delete (accessible name "Delete") acts on the // ACTIVE tab's file — no confirm step for files. await page.locator('#tree [data-path="/app/scratch/from-guest.txt"]').click(); await expect(tab(page, "/app/scratch/from-guest.txt")).toHaveAttribute("aria-selected", "true"); await page.locator("#btn-editor-delete").click(); await expect(page.locator("#status")).toContainText("deleted /app/scratch/from-guest.txt"); await expect(page.locator('#tree [data-path="/app/scratch/from-guest.txt"]')).toHaveCount(0); await expect(tab(page, "/app/scratch/from-guest.txt")).toHaveCount(0);
// Dir delete: context menu → inline confirm flow — no window.confirm anywhere. await ctxMenu(page, "/app/scratch", "delete"); await expect(page.locator("#confirm-row")).toBeVisible(); await expect(page.locator("#confirm-text")).toContainText("/app/scratch"); await page.locator("#confirm-yes").click(); await expect(page.locator("#status")).toContainText("deleted /app/scratch"); await expect(page.locator('#tree [data-path="/app/scratch"]')).toHaveCount(0);});
test("error surfacing: saving into a missing dir shows ENOENT in #status; the page stays usable", async ({ page }) => { await openHarness(page); await ctxMenu(page, "/app", "new-file"); await page.locator("#inline-input").fill("/app/nope/deep.txt"); // parent dir does not exist await page.locator("#inline-ok").click(); await expect(page.locator("#status")).toContainText("ENOENT"); // The failed create made nothing. await expect(page.locator('#tree [data-path="/app/nope"]')).toHaveCount(0);
// Nothing wedged: the runner still works and exits 0. await page.locator("#entry").fill("/app/writer.cjs"); await page.locator("#btn-run").click(); await expect(page.locator("#exit")).toHaveText("0", { timeout: 15_000 }); await expect(page.locator('#tree [data-path="/app/scratch/from-guest.txt"]')).toBeVisible({ timeout: 10_000 });});
test("tabs: dirty dot tracks edits, switching preserves unsaved edits, closing the dirty tab moves selection", async ({ page }) => { await openHarness(page);
// Two tabs: main.cjs first, then notes.md (active). await page.locator('#tree [data-path="/app/main.cjs"]').click(); await expect(tab(page, "/app/main.cjs")).toHaveAttribute("aria-selected", "true"); await expect(page.locator("#content")).toHaveValue(/node:fs/); const mainSource = await page.locator("#content").inputValue();
await page.locator('#tree [data-path="/app/notes.md"]').click(); await expect(tab(page, "/app/notes.md")).toHaveAttribute("aria-selected", "true"); await page.locator("#content").fill("unsaved tab edit\n"); // The dirty dot appears on the edited tab only. await expect(tab(page, "/app/notes.md")).toHaveAttribute("data-dirty", "true"); await expect(tab(page, "/app/notes.md").locator(".tab-dirty-dot")).toBeVisible(); await expect(tab(page, "/app/main.cjs")).toHaveAttribute("data-dirty", "false"); await expect(tab(page, "/app/main.cjs").locator(".tab-dirty-dot")).toBeHidden();
// Switch to main.cjs and back: the unsaved notes.md edit is preserved in // memory and re-shown. await tab(page, "/app/main.cjs").click(); await expect(page.locator("#content")).toHaveValue(mainSource); await tab(page, "/app/notes.md").click(); await expect(page.locator("#content")).toHaveValue("unsaved tab edit\n"); await expect(tab(page, "/app/notes.md")).toHaveAttribute("data-dirty", "true");
// Close the dirty tab: selection moves to the neighbor, and the tree keeps // notes.md — closing a tab is not deleting its file. await tab(page, "/app/notes.md").locator(".tab-close").click(); await expect(tab(page, "/app/notes.md")).toHaveCount(0); await expect(tab(page, "/app/main.cjs")).toHaveAttribute("aria-selected", "true"); await expect(page.locator("#content")).toHaveValue(mainSource); await expect(page.locator('#tree [data-path="/app/notes.md"]')).toBeVisible(); await expect(page.locator('#tree [data-path="/app/notes.md"]')).toHaveCount(1);});
// ---- editor-library slice ----// The assertion hook the brief prescribes: microlighter registers one CSS// Custom Highlight per token category over the mirror; expect.poll retries// until the async grammar load + rAF-debounced re-highlight have landed.const highlightCount = (page: Page) => () => page.evaluate<number>("CSS.highlights.size");
test("highlighting: opening a code file registers CSS Custom Highlights on the mirror", async ({ page }) => { await openHarness(page); await page.locator('#tree [data-path="/app/main.cjs"]').click(); await expect(tab(page, "/app/main.cjs")).toHaveAttribute("aria-selected", "true"); // The mirror is visible, carries the file's text, and at least one token // category is registered after the rAF-debounced highlight pass. await expect(page.locator("#hl-wrap")).toBeVisible(); await expect(page.locator("#hl-mirror")).toContainText("node:fs"); await expect.poll(highlightCount(page), { timeout: 5_000 }).toBeGreaterThan(0);});
test("highlighting: edits re-register highlights after a rAF (input → re-highlight)", async ({ page }) => { await openHarness(page); await page.locator('#tree [data-path="/app/greet.js"]').click(); await expect(tab(page, "/app/greet.js")).toHaveAttribute("aria-selected", "true"); await expect.poll(highlightCount(page), { timeout: 5_000 }).toBeGreaterThan(0); // Type a comment + a string; the mirror re-syncs on input and highlightAll // re-runs — no assertion on exact token counts, just that registration // survives the edit (a broken re-highlight would empty the registry). await page.locator("#content").fill('// harness probe\nconst neon = "#00ff87";\n'); await expect(page.locator("#hl-mirror")).toContainText("#00ff87"); await expect.poll(highlightCount(page), { timeout: 5_000 }).toBeGreaterThan(0);});
test("markdown: .md files edit in the textarea with a markdown highlight mirror", async ({ page }) => { await openHarness(page); await page.locator('#tree [data-path="/app/notes.md"]').click(); await expect(tab(page, "/app/notes.md")).toHaveAttribute("aria-selected", "true"); await expect(page.locator("#content")).toHaveValue("# scratch\n"); await expect(page.locator("#hl-wrap")).toBeVisible(); await expect(page.locator("#hl-mirror")).toHaveAttribute("data-language", "markdown"); await expect.poll(highlightCount(page), { timeout: 5_000 }).toBeGreaterThan(0);});
test("plain text: .txt files get no highlight mirror and edit normally", async ({ page }) => { await openHarness(page); await page.locator('#tree [data-path="/app/name.txt"]').click(); await expect(tab(page, "/app/name.txt")).toHaveAttribute("aria-selected", "true"); // No grammar for .txt → the mirror stays hidden and the textarea keeps its // own text color (no hl-on overlay). await expect(page.locator("#hl-wrap")).toBeHidden(); await expect(page.locator("#content")).toBeVisible(); await page.locator("#content").fill("plain text, no tokens here"); await expect(tab(page, "/app/name.txt")).toHaveAttribute("data-dirty", "true"); await expect(page.locator("#content")).toHaveValue("plain text, no tokens here"); await page.locator("#btn-save").click(); await expect(page.locator("#status")).toContainText("saved /app/name.txt"); await expect(tab(page, "/app/name.txt")).toHaveAttribute("data-dirty", "false");});