bimr-web/tests/ifcx-open.spec.ts
2026-09-01 10:37:51 +02:00

276 lines
9.9 KiB
TypeScript

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");
});