← back to Quadrille Showroom
scripts/walkthrough-record.js
116 lines
#!/usr/bin/env node
/**
* walkthrough-record.js — automated "move around" screen recording of the showroom.
* Drives a real Chromium through: load → walk to the wall (proximity book-match) →
* inspect a wing (new spec-sheet card) → fan cascade → Peruse auto-tour → scene stats.
* Records WebM video + key screenshots; convert to MP4 via ffmpeg afterward.
*
* Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/walkthrough-record.js
* Local 127.0.0.1 is auth-exempt, so no basic-auth needed.
*/
const path = require('path');
const fs = require('fs');
const { chromium } = require('playwright');
const URL = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
const OUT = path.join(__dirname, '..', 'recordings');
fs.mkdirSync(OUT, { recursive: true });
const SHOTS = path.join(OUT, 'shots');
fs.mkdirSync(SHOTS, { recursive: true });
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function holdKey(page, key, ms) {
await page.keyboard.down(key);
await sleep(ms);
await page.keyboard.up(key);
}
(async () => {
const browser = await chromium.launch({ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
recordVideo: { dir: OUT, size: { width: 1600, height: 900 } },
});
const page = await ctx.newPage();
const logs = [];
page.on('console', m => logs.push(`[${m.type()}] ${m.text()}`));
console.log('→ loading', URL);
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Wait for the engine to build the wing wall.
await page.waitForFunction(() => window._wingBoards && window._wingBoards.length > 0, { timeout: 25000 }).catch(() => {});
await sleep(2500); // let the loader fade + first textures stream in
await page.screenshot({ path: path.join(SHOTS, '01-overview.png') });
console.log(' wings:', await page.evaluate(() => (window._wingBoards || []).length));
// 1) Walk forward toward the wall — triggers proximity book-match on the nearest wing.
console.log('→ walking to the wall (W)');
await holdKey(page, 'KeyW', 1600);
await sleep(1200);
await page.screenshot({ path: path.join(SHOTS, '02-bookmatch.png') });
// Strafe a little so the footage "moves around".
await holdKey(page, 'KeyD', 700);
await holdKey(page, 'KeyA', 1100);
await sleep(900);
await page.screenshot({ path: path.join(SHOTS, '03-strafe.png') });
// 2) Inspect a wing by clicking near screen-centre (a wing face is dead ahead at the wall).
console.log('→ inspecting a wing (click)');
await page.mouse.click(800, 430);
await sleep(1800);
// The spec-sheet detail card should now be visible — capture it.
const detailVisible = await page.evaluate(() => {
const el = document.getElementById('wing-detail');
return el && !el.classList.contains('hidden');
});
await page.screenshot({ path: path.join(SHOTS, '04-spec-card.png') });
console.log(' spec card visible:', detailVisible);
// Pull the rendered spec rows so we can prove the specs surfaced.
const specText = await page.evaluate(() => {
const grid = document.getElementById('detail-spec-grid');
if (!grid) return null;
return [...grid.querySelectorAll('.wd-spec')].map(r => r.querySelector('dt').textContent + ': ' + r.querySelector('dd').textContent);
});
console.log(' SPEC SHEET:', JSON.stringify(specText));
// 3) Fan cascade to browse adjacent patterns.
console.log('→ fan cascade');
await page.click('#btn-fan-right').catch(() => {});
await sleep(1500);
await page.screenshot({ path: path.join(SHOTS, '05-fan.png') });
await page.keyboard.press('Escape');
await sleep(800);
// 4) Peruse — endless auto-tour. Flies wing→wing, opens each like a book, shows the card.
console.log('→ Peruse auto-tour (P) for ~14s');
await page.keyboard.press('KeyP');
for (let i = 0; i < 4; i++) {
await sleep(3500);
await page.screenshot({ path: path.join(SHOTS, `06-peruse-${i + 1}.png`) });
}
await page.keyboard.press('Escape');
await sleep(1000);
// Scene diagnostics — proves the render is healthy.
const stats = await page.evaluate(() => (window._getSceneStats ? window._getSceneStats() : 'n/a'));
const fps = await page.evaluate(() => { const el = document.getElementById('fps-counter'); return el ? el.textContent : 'n/a'; });
console.log('→ SCENE STATS:', JSON.stringify(stats));
console.log('→ FPS:', fps);
await page.close(); // finalizes the video file
const vid = await page.video().path().catch(() => null);
await ctx.close();
await browser.close();
// Rename the video deterministically.
let finalWebm = path.join(OUT, 'walkthrough.webm');
if (vid && fs.existsSync(vid)) { fs.renameSync(vid, finalWebm); }
fs.writeFileSync(path.join(OUT, 'walkthrough-stats.json'), JSON.stringify({ url: URL, stats, fps, specText, detailVisible, when: new Date().toISOString() }, null, 2));
fs.writeFileSync(path.join(OUT, 'walkthrough-console.log'), logs.join('\n'));
console.log('✓ video:', finalWebm);
console.log('✓ shots:', SHOTS);
})().catch(e => { console.error('RECORD FAILED:', e.message); process.exit(1); });