From e70c58fe992b4672e5c1d0078d52e8f42164f65b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 29 Jun 2026 17:44:02 +1000 Subject: [PATCH] ifcviewer-web: smoke-test the Blob.slice byte-range load path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second Playwright case that picks the sample sidecar through the file input, waits for the C side to confirm the blob load (console), then asserts an orbit drag changes the canvas with zero WebGPU errors. This exercises the #88 path distinctly from the embedded MEMFS sample — a broken metadata-head/tail or chunk range read renders blank and fails the orbit-changed-canvas check. Both cases pass. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer-web/tests/smoke.spec.mjs | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/ifcviewer-web/tests/smoke.spec.mjs b/src/ifcviewer-web/tests/smoke.spec.mjs index 3b6529023c..7fab5a55a1 100644 --- a/src/ifcviewer-web/tests/smoke.spec.mjs +++ b/src/ifcviewer-web/tests/smoke.spec.mjs @@ -1,4 +1,8 @@ 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 @@ -60,3 +64,57 @@ test('renders the sample and orbits without WebGPU errors', async ({ page }) => // 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([]); +});