// GPU-versus-CPU test: serves the app with Vite's dev server, opens it in a // headless Chromium with WebGPU through Playwright, and reads the result of the // in-page self-test (src/selftest.ts), which compares one step of the WGSL // kernel with the float64 reference on several presets. Then runs short // strikes on a free plate and on a mounted cymbal to completion. import { test } from "node:test"; import assert from "node:assert/strict"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { createServer } from "vite"; import { chromium, type Page } from "playwright"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); interface SelfTestCase { name: string; accError: { u: number; v: number; w: number }; positionError: number; nodes: number; dt: number; } interface SelfTestResult { done: boolean; passed?: boolean; cases?: SelfTestCase[]; error?: string } interface Status { phase: string; t: number; totalSteps: number; wmax: number; peakPressure: number; samples: number; frames: number; stepsPerSec: number; dt: number; nx: number; ny: number; stick: { maxForce: number }; design: { name: string | null; nodes: number; mounted: boolean; error: string | null }; } const ARGS = ["--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--ignore-gpu-blocklist", "--no-sandbox"]; /** Serves the app and opens `query` in a headless Chromium; `body` gets the page and the list of page errors. */ async function withPage(query: string, body: (page: Page, errors: string[]) => Promise): Promise { const server = await createServer({ root, logLevel: "silent", server: { port: 0, strictPort: false } }); await server.listen(); const url = server.resolvedUrls?.local[0]; assert.ok(url, "dev server URL"); const browser = await chromium.launch({ headless: true, args: ARGS }); try { const page = await browser.newPage(); const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); page.on("console", (msg) => { if (msg.type() === "error" || msg.type() === "warning") errors.push(`console.${msg.type()}: ${msg.text()}`); }); await page.goto(`${url}?${query}`); await body(page, errors); } finally { await browser.close(); await server.close(); } } test("one GPU step matches the float64 reference on flat, bowed, tapered, mounted, and elliptical plates", { timeout: 180_000 }, async () => { await withPage("selftest=1", async (page, errors) => { let result: SelfTestResult = { done: false }; for (let i = 0; i < 240 && !result.done; i++) { await page.waitForTimeout(500); result = await page.evaluate("window.steelsim ? window.steelsim.selftest() : { done: false }"); } assert.deepEqual(errors, [], "page errors"); assert.ok(result.done, "self-test did not finish"); assert.equal(result.error, undefined, `self-test error: ${result.error}`); for (const c of result.cases ?? []) { console.log(` ${c.name}: ${c.nodes} nodes, dt ${(c.dt * 1e6).toFixed(2)} us, acc errors ${c.accError.u.toExponential(1)} / ${c.accError.v.toExponential(1)} / ${c.accError.w.toExponential(1)}, position ${c.positionError.toExponential(1)}`); } assert.ok(result.passed, `self-test failed: ${JSON.stringify(result.cases)}`); }); }); /** Runs a short simulation to completion and returns its final status. */ async function runToCompletion(query: string): Promise { let st: Status | null = null; await withPage(`autostart=1&${query}`, async (page, errors) => { for (let i = 0; i < 600; i++) { await page.waitForTimeout(250); st = await page.evaluate("window.steelsim && window.steelsim.status() ? { ...window.steelsim.status(), design: window.steelsim.design() } : null"); const noGpu = await page.evaluate("window.steelsim.noGpu()"); assert.equal(noGpu, null, `no WebGPU: ${noGpu}`); if (st && st.phase === "done") break; } assert.deepEqual(errors, [], "page errors"); }); assert.ok(st, "no status"); return st!; } test("a short strike on the free bowed rectangle completes with finite output", { timeout: 180_000 }, async () => { const st = await runToCompletion("preset=bowed-rectangle&spacing=0.012&duration=0.2&rigidProjection=1"); console.log(` ${st.design.name}: ${st.nx}x${st.ny}, dt ${(st.dt * 1e6).toFixed(2)} us, ${(st.stepsPerSec / 1e3).toFixed(0)} k steps/s, peak ${st.peakPressure.toFixed(3)} Pa, max |w| ${(st.wmax * 1e3).toFixed(2)} mm, max contact force ${st.stick.maxForce.toFixed(1)} N`); assert.equal(st.design.name, "Bowed steel rectangle", "the preset was read from the URL"); assert.equal(st.design.mounted, false); assert.equal(st.phase, "done"); assert.ok(st.samples >= 0.2 * 48000 - 1, "recorded the requested duration"); assert.ok(isFinite(st.wmax) && st.wmax < 0.05, "displacement stays bounded"); assert.ok(st.peakPressure > 1e-3 && isFinite(st.peakPressure), "the plate radiates"); assert.ok(st.stick.maxForce > 1, "the stick hit the plate"); }); test("a short strike on the mounted, tapered 16 inch crash completes with finite output", { timeout: 180_000 }, async () => { const st = await runToCompletion("preset=crash-16&spacing=0.012&duration=0.2"); console.log(` ${st.design.name}: ${st.nx}x${st.ny}, ${st.design.nodes} nodes, ${(st.stepsPerSec / 1e3).toFixed(0)} k steps/s, peak ${st.peakPressure.toFixed(3)} Pa, max |w| ${(st.wmax * 1e3).toFixed(2)} mm, max contact force ${st.stick.maxForce.toFixed(1)} N`); assert.equal(st.design.name, "16 inch crash"); assert.equal(st.design.mounted, true); assert.equal(st.phase, "done"); assert.ok(isFinite(st.wmax) && st.wmax < 0.05, "displacement stays bounded"); assert.ok(st.peakPressure > 1e-3, "the cymbal radiates"); assert.ok(st.stick.maxForce > 1, "the stick hit the cymbal"); }); test("a script with an error is reported and does not start", { timeout: 120_000 }, async () => { await withPage("", async (page, errors) => { for (let i = 0; i < 40; i++) { await page.waitForTimeout(250); const ready = await page.evaluate("!!window.steelsim"); if (ready) break; } await page.evaluate("window.steelsim.setScript('design.material.E = 1e11; design.material.nu = 0.3; design.material.rho = 8000; design.bounds.width = 0.3; design.bounds.height = 0.2; design.thickness = -1;')"); await page.waitForTimeout(200); const d = await page.evaluate("window.steelsim.design()"); assert.equal(d.name, null); assert.match(d.error ?? "", /thickness must be positive/); await page.evaluate("window.steelsim.start()"); await page.waitForTimeout(500); assert.equal(await page.evaluate("window.steelsim.status()"), null, "nothing started"); assert.deepEqual(errors, [], "page errors"); }); });