import { test, expect } from "@playwright/test"; import { readFileSync } from "fs"; function spyViewer() { return ` Object.defineProperty(window, "__bimrViewer", { configurable: true, set(v) { const record = []; (window).__addedModels = record; v.addModel = (name, m) => { record.push({ name, m }); }; this._v = v; }, get() { return this._v; }, }); `; } test("open .ifcx file displays in viewer and comments the editor", async ({ page }) => { // Intercept the viewer registration (render.mjs) to record every addModel call. await page.addInitScript(spyViewer()); await page.goto("/"); const ifcx = JSON.stringify({ header: { id: "test.ifcx" }, layers: [] }); await page.locator("#file-open").setInputFiles({ name: "test.ifcx", mimeType: "application/json", buffer: Buffer.from(ifcx), }); await expect(page.locator("#editor .cm-content")).toContainText("# test.ifcx"); await expect(page.locator("#output")).toContainText("Loaded: test.ifcx"); await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter( (a: { name: string }) => a.name === "loaded", ), ), ) .toHaveLength(1); const loaded = await page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "loaded"), ); expect(loaded[0].m.header.id).toBe("test.ifcx"); }); test("open .py file still loads into the editor", async ({ page }) => { await page.goto("/"); await page.locator("#file-open").setInputFiles({ name: "script.py", mimeType: "text/x-python", buffer: Buffer.from("print('hi')\n"), }); await expect(page.locator("#editor .cm-content")).toContainText("print('hi')"); const name = await page.evaluate(() => (window as any).__bimrActiveTabName()); expect(name).toBe("script.py"); }); test("New clears stale bimr/ifcx buffers", async ({ page }) => { await page.addInitScript(spyViewer()); await page.addInitScript(() => localStorage.setItem("bimr.runMode", "manual")); await page.goto("/"); // Produce data in the buffers by running the default script. await page.locator("#btn-run").click(); await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ), ) .toBeGreaterThan(0); await page.locator("#btn-view-ifcx").click(); await expect(page.locator("#editor .cm-content")).toContainText('"data"'); // New -> Erase current await page.locator("#menu-file-trigger").click(); await page.locator("#menu-new").click(); await page.locator("#new-dialog-erase").click(); // py buffer emptied. await expect(page.locator("#editor .cm-content")).toHaveText(""); // ifcx buffer cleared — shows the placeholder, not stale data. await page.locator("#btn-view-ifcx").click(); await expect(page.locator("#editor .cm-content")).toContainText("no model yet"); }); test("Save submenu disables unavailable buffers and downloads the chosen one", async ({ page }) => { await page.addInitScript(spyViewer()); await page.addInitScript(() => localStorage.setItem("bimr.runMode", "manual")); await page.goto("/"); // Wait for the idle warm-up run to settle, then New clears the buffers. await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ), ) .toBeGreaterThan(0); await page.locator("#menu-file-trigger").click(); await page.locator("#menu-new").click(); await page.locator("#new-dialog-erase").click(); // Cleared buffers -> bimr/ifcx saves disabled, python always enabled. await page.locator("#menu-file-trigger").click(); await page.locator("#menu-save").click(); await expect(page.locator("#menu-save-python")).not.toBeDisabled(); await expect(page.locator("#menu-save-bimr")).toBeDisabled(); await expect(page.locator("#menu-save-ifcx")).toBeDisabled(); // Load a real sample and run it to populate the bimr/ifcx buffers. await page.locator("#btn-view-python").click(); await page.locator("#menu-examples-trigger").click(); await page.locator("#menu-examples-dropdown li button").first().click(); await expect(page.locator("#editor .cm-content")).toContainText("Welcome to BIMR.NET!"); await page.locator("#btn-run").click(); await expect(page.locator("#menu-save-bimr")).not.toBeDisabled({ timeout: 20000 }); await expect(page.locator("#menu-save-ifcx")).not.toBeDisabled(); // Download the bimr buffer. await page.locator("#menu-file-trigger").click(); await page.locator("#menu-save").click(); const downloadPromise = page.waitForEvent("download"); await page.locator("#menu-save-bimr").click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe("building.bimr"); const bimrText = readFileSync((await download.path())!, "utf8"); expect(bimrText.length).toBeGreaterThan(0); }); test("opening a real .ifcx does not auto-run or empty the ifcx view", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // Wait for the idle auto-run to have produced a live model. await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ), ) .toBeGreaterThan(0); // Capture the real app-generated IFCX via File > Save > Ifcx. const downloadPromise = page.waitForEvent("download"); await page.locator("#menu-file-trigger").click(); await page.locator("#menu-save").click(); await page.locator("#menu-save-ifcx").click(); const download = await downloadPromise; const ifcxText = readFileSync((await download.path())!, "utf8"); expect(ifcxText).toContain('"data"'); const liveBefore = await page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ); await page.locator("#file-open").setInputFiles({ name: "my-model.ifcx", mimeType: "application/json", buffer: Buffer.from(ifcxText), }); await expect(page.locator("#editor .cm-content")).toContainText("# my-model.ifcx"); await expect(page.locator("#output")).toContainText("Loaded: my-model.ifcx"); // Wait past the run debounce — opening an ifcx must NOT trigger a new run. await page.waitForTimeout(800); const liveAfter = await page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ); expect(liveAfter).toBe(liveBefore); // The ifcx view must show the loaded model's data, not an empty ifcx. await page.locator("#btn-view-ifcx").click(); await expect(page.locator("#editor .cm-content")).toContainText('"data"'); }); test("open .bimr file compiles, displays in viewer and loads source into the editable bimr view", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); const bimrSrc = `p1 = Point(0, 0, 0) p2 = Point(1000, 0, 0) p3 = Point(1000, 800, 0) p4 = Point(0, 800, 0) l1 = Line(p1, p2) l2 = Line(p2, p3) l3 = Line(p3, p4) l4 = Line(p4, p1) w1 = Wall(l1, 20, 300) w2 = Wall(l2, 20, 300) w3 = Wall(l3, 20, 300) w4 = Wall(l4, 20, 300)`; await page.locator("#file-open").setInputFiles({ name: "house.bimr", mimeType: "text/plain", buffer: Buffer.from(bimrSrc), }); await expect(page.locator("#output")).toContainText("Loaded: house.bimr"); await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter( (a: { name: string }) => a.name === "loaded", ), ), ) .toHaveLength(1); const loaded = await page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "loaded"), ); const data = loaded[0].m.data as unknown[]; expect(data.length).toBe(4); // The source must be loaded into the editable bimr view, ready to tweak. await expect(page.locator("#editor .cm-content")).toContainText("w1 = Wall(l1, 20, 300)"); await expect(page.locator("#btn-view-bimr")).toHaveClass(/active/); }); test("bimr mode: editing DSL source and running compiles to a model", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); await page.locator("#btn-view-bimr").click(); const editor = page.locator("#editor .cm-content"); await editor.click(); await page.keyboard.press("Control+a"); await page.keyboard.insertText( "p1 = Point(0,0,0)\np2 = Point(5000,0,0)\nl1 = Line(p1,p2)\nw1 = Wall(l1,200,3000)", ); await expect(page.locator("#output")).toContainText("Compiled 1 entity", { timeout: 15000 }); await expect .poll(() => page.evaluate(() => ((window as any).__addedModels ?? []).filter((a: { name: string }) => a.name === "live").length, ), ) .toBeGreaterThan(0); await expect(page.locator("#menu-save-bimr")).not.toBeDisabled(); }); test("bimr mode: invalid DSL source shows an error", async ({ page }) => { await page.addInitScript(spyViewer()); await page.addInitScript(() => localStorage.setItem("bimr.runMode", "manual")); await page.goto("/"); await page.locator("#btn-view-bimr").click(); const editor = page.locator("#editor .cm-content"); await editor.click(); await page.keyboard.press("Control+a"); await page.keyboard.insertText("w1 = Wall(missing)"); await page.keyboard.press("Control+Enter"); await expect(page.locator("#output")).toContainText("Error:", { ignoreCase: true }); }); test("open invalid .bimr file shows an error and does not touch the editor", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); await page.locator("#file-open").setInputFiles({ name: "broken.bimr", mimeType: "text/plain", buffer: Buffer.from("p1 = Point(0, 0, 0)\nunknown = Frobnicate(p1)\n"), }); await expect(page.locator("#output")).toContainText("Error"); await expect(page.locator("#editor .cm-content")).not.toContainText("# broken.bimr"); });