Files
IfcOpenShell/src/ifcviewer-web/tests/smoke.spec.mjs
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

162 lines
6.4 KiB
JavaScript
Raw Normal View History

import { test, expect } from '@playwright/test';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// End-to-end smoke test for the WebGPU build. Every bug from the bring-up
// of camera input + file loading was exactly this shape — the page would
// load but render blank, spam uncaptured WebGPU errors, or swallow mouse
// input behind an overlay. This test would have caught all of them.
// Capture the composited canvas pixels as a PNG buffer. We use
// Playwright's element screenshot rather than an in-page drawImage/
// toDataURL: reading a WebGPU canvas back through a 2D context returns
// blank (the swap-chain texture isn't persisted for 2D copy), whereas
// the browser's own capture path includes GPU layers.
async function shot(page) {
return page.locator('#viewer-canvas').screenshot();
}
test('renders the sample and orbits without WebGPU errors', async ({ page }) => {
const gpuErrors = [];
page.on('console', (msg) => {
const t = msg.text();
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(t)) {
gpuErrors.push(t);
}
});
page.on('pageerror', (e) => gpuErrors.push('pageerror: ' + e.message));
await page.goto('/IfcViewerWeb.html');
// Init is complete once C publishes the app pointer (set in the wgpu
// device callback). If WebGPU is missing or the device promise stalls,
// this times out — a real failure, not a flake.
await page.waitForFunction(
() => !!(window.Module && window.Module._app_ptr),
null,
{ timeout: 30_000 },
);
// Let a few frames render the embedded sample.
await page.waitForTimeout(1000);
// Drag across the canvas centre to orbit. The composited canvas must
// change — a single assertion that simultaneously proves the scene
// rendered (a blank canvas dragged stays blank), input is wired, and
// the log overlay isn't intercepting mouse events over the viewport.
const box = await page.locator('#viewer-canvas').boundingBox();
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
const before = await shot(page);
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 140, cy + 50, { steps: 10 });
await page.mouse.up();
await page.waitForTimeout(500);
const after = await shot(page);
expect(
Buffer.compare(before, after),
'orbit drag did not change the canvas — blank render or dead input',
).not.toBe(0);
// No WebGPU validation/OOM noise at any point.
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
});
test('loads a user-picked sidecar through the Blob.slice byte-range path', async ({ page }) => {
// Exercises #88: the picked File is read via Blob.slice (metadata head/tail
// + per-chunk byte ranges) WITHOUT copying the whole file into the wasm
// heap. Distinct code path from the embedded MEMFS sample above, so it
// needs its own coverage — a broken blob read renders blank.
const gpuErrors = [];
page.on('console', (msg) => {
const t = msg.text();
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(t)) {
gpuErrors.push(t);
}
});
page.on('pageerror', (e) => gpuErrors.push('pageerror: ' + e.message));
await page.goto('/IfcViewerWeb.html');
await page.waitForFunction(
() => !!(window.Module && window.Module._app_ptr),
null,
{ timeout: 30_000 },
);
// Pick the sample sidecar through the hidden file input. setInputFiles
// hands the page a real File, so the browser's Blob.slice reads it exactly
// as it would a user's 200-500 MB sidecar — just smaller. Wait for the C
// side to confirm the blob load landed (logged to stderr → console).
const samplePath = resolve(__dirname, '..', 'sample.ifcview');
const loaded = page.waitForEvent('console', {
predicate: (m) => /loaded blob sidecar/.test(m.text()),
timeout: 15_000,
});
await page.locator('#file-input').setInputFiles(samplePath);
await loaded;
await page.waitForTimeout(800); // let the chunk stream + a few frames draw
// Orbit: the composited canvas must change, proving the blob-streamed
// geometry rendered and input still drives it.
const box = await page.locator('#viewer-canvas').boundingBox();
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
const before = await shot(page);
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 140, cy + 50, { steps: 10 });
await page.mouse.up();
await page.waitForTimeout(500);
const after = await shot(page);
expect(
Buffer.compare(before, after),
'orbit after blob load did not change the canvas — blob read or stream failed',
).not.toBe(0);
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
});
test('click selects an object and the highlight renders (async pick)', async ({ page }) => {
// Exercises the async object-pick readback: a click maps the pick staging
// buffer via a spontaneous callback (no blocking spin, which would hang the
// page), routes object_id through selection, and the next render flushes the
// highlight. A broken async pick either hangs init/never selects (canvas
// unchanged) or spams WebGPU errors.
const gpuErrors = [];
page.on('console', (msg) => {
const t = msg.text();
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(t)) {
gpuErrors.push(t);
}
});
page.on('pageerror', (e) => gpuErrors.push('pageerror: ' + e.message));
await page.goto('/IfcViewerWeb.html');
await page.waitForFunction(
() => !!(window.Module && window.Module._app_ptr),
null,
{ timeout: 30_000 },
);
await page.waitForTimeout(1000); // sample framed + a few frames drawn
// Click dead-centre, where the framed sample geometry sits. A plain
// down+up at one point is a pick (no drag), so it must change the canvas
// (selection highlight). If centre happens to miss, the assertion guards it.
const box = await page.locator('#viewer-canvas').boundingBox();
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
const before = await shot(page);
await page.mouse.click(cx, cy);
await page.waitForTimeout(600); // async pick result + flush + render
const after = await shot(page);
expect(
Buffer.compare(before, after),
'click did not change the canvas — async pick failed or hit empty space',
).not.toBe(0);
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
});