import assert from 'node:assert/strict'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; import * as THREE from '../web/react/node_modules/three/build/three.module.js'; import { GLTFExporter } from '../web/react/node_modules/three/examples/jsm/exporters/GLTFExporter.js'; globalThis.FileReader = class { readAsArrayBuffer(blob) { blob.arrayBuffer().then((result) => { this.result = result; this.onloadend?.(); }); } readAsDataURL(blob) { blob.arrayBuffer().then((result) => { this.result = `data:${blob.type};base64,${Buffer.from(result).toString('base64')}`; this.onloadend?.(); }); } }; const pw = process.env.PLAYWRIGHT_MODULE ? await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href) : await import('playwright'); const browser = await (pw.default ?? pw).chromium.launch({ executablePath: process.env.CHROME_PATH ?? '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const baseline = process.env.RENDER_BASELINE === '1'; const output = new URL(`../coverage/glb-machine-manager/material-rendering/${baseline ? 'before' : 'after'}/`, import.meta.url); await mkdir(output, { recursive: true }); const page = await browser.newPage({ viewport: { width: 1600, height: 1100 } }); page.setDefaultTimeout(30000); const report = { status: 'running', baseline, checks: [], errors: [] }; page.on('pageerror', (e) => report.errors.push(e.message)); const c = (id) => page.locator(`[data-axis-control="${id}"]`); const preview = page.locator('[data-machine-manager-preview]'); async function importAndCapture(file, stem) { await c('machine-manager-files').setInputFiles(file); await page.waitForFunction(() => document.querySelector('.machine-manager-workbench')?.getAttribute('aria-busy') === 'false' && document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount === '1'); await c('machine-view-fit').click(); await page.waitForFunction(() => document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerView === 'fit' && document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerRenderState === 'rendered'); await preview.screenshot({ path: fileURLToPath(new URL(`${stem}.png`, output)) }); } try { await page.goto(process.env.MACHINE_MANAGER_URL ?? 'http://127.0.0.1:5173/'); await page.waitForFunction(() => document.querySelector('main')?.dataset.axisProgramLoading === 'false'); await c('machine-manager-settings').click(); await importAndCapture(fileURLToPath(new URL('../glb/m1.glb', import.meta.url)), 'm1'); report.manager = await preview.evaluate((el) => ({ lighting: el.dataset.machineManagerLighting, toneMapping: el.dataset.machineManagerToneMapping, exposure: el.dataset.machineManagerExposure, meshes: Number(el.dataset.machineManagerSceneMeshes) })); assert.equal(await page.locator('.machine-axis-overview .bound').count(), 5); if (!baseline) { assert.equal(report.manager.toneMapping, 'AgX'); assert.ok(Number(report.manager.exposure) <= 1); // Exercise export from the actual manager: render settings must not recolor saved materials. const downloadPromise = page.waitForEvent('download'); await c('machine-manager-export').click(); const saved = await readFile(await (await downloadPromise).path()); const original = await readFile(new URL('../glb/m1.glb', import.meta.url)); const json = (buffer) => JSON.parse(buffer.toString('utf8', 20, 20 + buffer.readUInt32LE(12))); const materials = json(saved).materials, sourceMaterials = json(original).materials; for (const source of sourceMaterials) { const target = materials.find((m) => m.name === source.name); assert.ok(target, source.name); const a = source.pbrMetallicRoughness ?? {}, b = target.pbrMetallicRoughness ?? {}; for (const key of ['metallicFactor', 'roughnessFactor']) assert.ok(Math.abs((a[key] ?? 1) - (b[key] ?? 1)) < 1e-6, `${source.name}/${key}`); const color = a.baseColorFactor ?? [1, 1, 1, 1], savedColor = b.baseColorFactor ?? [1, 1, 1, 1]; color.forEach((value, i) => assert.ok(Math.abs(value - savedColor[i]) < 1e-6, `${source.name}/color${i}`)); if (source.normalTexture) assert.ok(target.normalTexture, `${source.name}/normal map`); } report.checks.push('manager export retains source PBR colors, roughness, metalness and normal maps'); } await c('machine-document-new').click(); const fixture = new THREE.Scene(); const swatches = [0xff0000, 0x00ff00, 0x0000ff, 0xbdbdbd, 0xffffff, 0x222222]; for (let i = 0; i < swatches.length; i++) { const mesh = new THREE.Mesh(new THREE.SphereGeometry(.13, 48, 24), new THREE.MeshStandardMaterial({ color: swatches[i], roughness: i === 3 ? .22 : .38, metalness: i === 3 ? 1 : 0 })); mesh.name = `material-probe-${i}`; mesh.position.set((i % 3 - 1) * .35, i < 3 ? 0 : .35, .2); fixture.add(mesh); } const buffer = Buffer.from(await new GLTFExporter().parseAsync(fixture, { binary: true })); await importAndCapture({ name: 'rgb-material-probes.glb', mimeType: 'model/gltf-binary', buffer }, 'rgb-materials'); fixture.traverse((object) => { object.geometry?.dispose(); object.material?.dispose(); }); report.checks.push('real m1 and saturated RGB / white / metal / dark material probes render without browser exceptions'); if (!baseline) { await page.keyboard.press('Escape'); await c('machine-glb-settings').click(); await c('machine-glb-file').setInputFiles(fileURLToPath(new URL('../glb/m1.glb', import.meta.url))); await page.waitForFunction(() => document.querySelector('.xyzac-machine-view')?.dataset.machineGlb && document.querySelector('main')?.dataset.axisProgramLoading === 'false'); await page.keyboard.press('Escape'); const viewer = page.locator('.xyzac-machine-view'); assert.equal(await viewer.locator('canvas').getAttribute('data-machine-tone-mapping'), 'AgX'); await viewer.screenshot({ path: fileURLToPath(new URL('controller-m1.png', output)) }); report.checks.push('control panel uses the same AgX neutral studio for imported GLBs'); } assert.deepEqual(report.errors, []); report.status = 'verified'; } catch (error) { report.status = 'failed'; report.error = String(error.stack ?? error); throw error; } finally { await writeFile(new URL('browser.report.json', output), JSON.stringify(report, null, 2)); await browser.close(); } console.log(JSON.stringify(report));