bimr-web/tests/compile.spec.ts
2026-08-31 10:49:15 +02:00

265 lines
8.7 KiB
TypeScript

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<string | null> {
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<string[] | null> {
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")}`),
);
});