import { test, expect } from "@playwright/test"; const WALL_SRC = `p1 = Point(0,0,0) p2 = Point(5000,0,0) l1 = Line(p1,p2) w1 = Wall(l1,200,3000)`; const INVALID_SRC = `w1 = Wall(missing)`; 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; }, }); `; } async function loadScript(page, src: string) { // The editor builds after the default sample fetch resolves (top-level // await in main.ts) — dispatching before that races the open-file // listener registration. Wait for the editor to hold content. await page.waitForFunction( () => (document.querySelector(".cm-content")?.textContent ?? "").length > 0, undefined, { timeout: 15000 }, ); await page.evaluate((text) => { window.dispatchEvent(new CustomEvent("bimr-open-file", { detail: { text, name: "test.py" }, })); }, src); } // Path of the last live model rendered by the viewer — null while nothing // has been compiled yet. Total: never throws, never returns undefined. async function lastLivePath(page): Promise { return page.evaluate(() => { const live = ((window as any).__addedModels ?? []).filter( (a: { name: string }) => a.name === "live", ); if (!live.length) return null; return live[live.length - 1].m?.data?.[0]?.path ?? null; }); } // Paths of the last live model — null while nothing has been compiled yet. async function livePaths(page): Promise { return page.evaluate(() => { const live = ((window as any).__addedModels ?? []).filter( (a: { name: string }) => a.name === "live", ); if (!live.length) return null; return (live[live.length - 1].m?.data ?? []).map((d: { path: string }) => d.path); }); } // Python-mode success shows the script's prints in #output (empty for // printless scripts) — the model itself is asserted via the viewer spy, // which works headless without WebGL. test("compiles a wall to IFCX", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); await loadScript(page, WALL_SRC); await expect.poll(() => lastLivePath(page), { timeout: 20000 }).toBe("wall-001"); }); test("shows error for invalid input", async ({ page }) => { await page.goto("/"); const output = page.locator("#output"); await loadScript(page, INVALID_SRC); await expect(output).toContainText("Error:", { ignoreCase: true, timeout: 15000 }); }); test("python mode — wall produces IFCX through the batch bridge", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // Python is the default mode — load wall source directly await loadScript(page, WALL_SRC); await expect.poll(() => lastLivePath(page), { timeout: 20000 }).toBe("wall-001"); }); test("python mode — error for invalid script", async ({ page }) => { await page.goto("/"); const output = page.locator("#output"); // Load invalid script, then wait for error in output await loadScript(page, INVALID_SRC); await expect(output).toContainText("Error:", { ignoreCase: true, timeout: 15000 }); }); test("updates live on input", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); await loadScript(page, WALL_SRC); await expect.poll(() => lastLivePath(page), { timeout: 20000 }).toBe("wall-001"); }); test("python mode — 4x4 column grid lifts over a List", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The column_grid sample's Python form: List of Points, Column lifts. const GRID_SRC = `pts = [Point(x * 1000, y * 1000, 0) for x in range(4) for y in range(4)] grid = List(*pts) cols = Column(grid, 300, 30, 30)`; await loadScript(page, GRID_SRC); const columnPaths = Array.from( { length: 16 }, (_, i) => `column-${String(i + 1).padStart(3, "0")}`, ); await expect.poll(() => livePaths(page), { timeout: 20000 }).toEqual(columnPaths); }); test("python mode — 12 columns lift over a Divide", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The columns_ring sample: Circle → Divide (the engine creates the // points) → Column lifts over the record's items. const RING_SRC = `center = Point(0, 0, 0) circle = Circle(center, 2000) ring = Divide(circle, 12) cols = Column(ring, 300, 30, 30)`; await loadScript(page, RING_SRC); const columnPaths = Array.from( { length: 12 }, (_, i) => `column-${String(i + 1).padStart(3, "0")}`, ); await expect.poll(() => livePaths(page), { timeout: 20000 }).toEqual(columnPaths); }); test("python mode — cylinder: Circle, Cut, Extrusion", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The cylinder sample: Cut creates the pieces, the Extrusion sweeps them. const CYL_SRC = `center = Point(0, 0, 0) circle = Circle(center, 1000) pieces = Cut(circle, 16) axis = Vector(0, 0, 3000) cylinder = Extrusion(pieces, axis)`; await loadScript(page, CYL_SRC); await expect.poll(() => livePaths(page), { timeout: 20000 }).toEqual([ "extrusion-001", ]); }); test("python mode — a cylinder of frames: Cut, Explode, Frame lift", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The frames_cylinder sample: the Cut pieces explode into pairs, the // Frame lifts one frame per pair — no generated names referenced. const RING_SRC = `center = Point(0, 0, 0) circle = Circle(center, 1000) pieces = Cut(circle, 12) pairs = Explode(pieces) frames = Frame(pairs, 100, 300, 20, 10)`; await loadScript(page, RING_SRC); const framePaths = Array.from( { length: 12 }, (_, i) => `frame-${String(i + 1).padStart(3, "0")}`, ); await expect.poll(() => livePaths(page), { timeout: 20000 }).toEqual(framePaths); }); test("python mode — peanut: Curve, Cut, Extrusion", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The peanut sample: a closed curve through eight controls, cut and // extruded — the deformed cylinder. const PEANUT_SRC = `c0 = Point(1600, 0, 0) c1 = Point(600, 500, 0) c2 = Point(0, 250, 0) c3 = Point(-600, 500, 0) c4 = Point(-1600, 0, 0) c5 = Point(-600, -500, 0) c6 = Point(0, -250, 0) c7 = Point(600, -500, 0) ctrl = List(c0, c1, c2, c3, c4, c5, c6, c7) curve = Curve(ctrl) pieces = Cut(curve, 24) axis = Vector(0, 0, 3000) peanut = Extrusion(pieces, axis)`; await loadScript(page, PEANUT_SRC); await expect.poll(() => livePaths(page), { timeout: 20000 }).toEqual([ "extrusion-001", ]); }); test("python mode — the building: Curve, Cut, Frame, Slab, Storey, Building", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // The building sample: per storey a curve cut into frames (no Each) and // a slab; Storeys gathered into a Building. const BLD_SRC = `N_LEVELS = 4 N_PIECES = 24 FRAME_W, FRAME_H, FRAME_D, FRAME_T = 200, 180, 35, 10 SLAB_T = 150 LEVEL_H = FRAME_H + 2 * SLAB_T BASE = [(-1500, -300), (0, 1000), (850, 800), (300, 0), (500, -800), (0, -1000), (-500, -800), (-300, 0)] levels = [] for lvl in range(N_LEVELS): z = lvl * LEVEL_H base = List(*[Point(x, y, z) for x, y in BASE]) pts = Random(base, seed=128 + lvl, min=-150, max=150, dims=2) curve = Curve(pts) Slab(curve, SLAB_T, z) pieces = Cut(curve, N_PIECES) pairs = Explode(pieces) frames = Frame(pairs, FRAME_W, FRAME_H, FRAME_D, FRAME_T) levels.append(Storey(z, frames)) building = Building("building-03", List(*levels))`; await loadScript(page, BLD_SRC); // 4 slabs + 4 x 24 frames = 100 mesh entries, slabs first per storey. await expect.poll(() => livePaths(page), { timeout: 30000 }).toHaveLength(100); const first = await page.evaluate(() => { const live = ((window as any).__addedModels ?? []).filter( (a: { name: string }) => a.name === "live", ); return live.length ? live[live.length - 1].m?.data?.[0]?.path : null; }); expect(first).toBe("slab-001"); }); test("python mode — 40x40 grid stays inside the MicroPython heap", async ({ page }) => { await page.addInitScript(spyViewer()); await page.goto("/"); // 1600 lifted columns — the IFCX document (~4.7MB) must never cross the // JS→MicroPython boundary (regression: compile() used to return it). const GRID_SRC = `pts = [Point(x * 100, y * 100, 0) for x in range(40) for y in range(40)] grid = List(*pts) cols = Column(grid, 300, 30, 30)`; await loadScript(page, GRID_SRC); await expect.poll(() => livePaths(page), { timeout: 30000 }).toEqual( Array.from({ length: 1600 }, (_, i) => `column-${String(i + 1).padStart(3, "0")}`), ); });