// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173. // Verifies the landing intro-video trigger + modal (lazy load, play, Esc/backdrop close, focus, layouts). const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); const out = path.resolve(__dirname, '../.verify/intro-video-20261109'); fs.mkdirSync(out, { recursive: true }); const report = { checks: [], errors: [], metrics: {} }; let ws, send; const delay = ms => new Promise(r => setTimeout(r, ms)); (async () => { const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json(); ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; }); let seq = 0; const pending = new Map(); ws.onmessage = ({ data }) => { const m = JSON.parse(data), p = pending.get(m.id); if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); } if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text); }; send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq, timer = setTimeout(() => reject(new Error('Timeout ' + method)), 30000); pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params })); }); const ev = async expression => { const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true, userGesture: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text); return r.result.value; }; const until = async (expression, tries = 120) => { for (let i = 0; i < tries; i++) { if (await ev(expression)) return; await delay(150); } throw new Error('Wait ' + expression); }; const check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); }; const shot = async name => { const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); }; const viewport = (width, height, mobile = false) => send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile }); const load = async () => { await send('Page.navigate', { url: process.env.PIERZAINER_URL || 'http://127.0.0.1:5173/' }); await until(`!!document.querySelector('.landing-video-trigger')`); await delay(1800); // entry animations }; const mp4Requests = `performance.getEntriesByType('resource').filter(e=>e.name.includes('pierzainer-intro')).length`; const pressEsc = async () => { for (const type of ['keyDown', 'keyUp']) await send('Input.dispatchKeyEvent', { type, key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); }; await send('Page.enable'); await send('Runtime.enable'); await viewport(1600, 1000); await load(); await shot('01-landing-1600'); report.metrics.triggers = await ev(`[...document.querySelectorAll('.landing-guide-trigger,.landing-video-trigger')].map(e=>{const r=e.getBoundingClientRect();return[e.className,Math.round(r.left),Math.round(r.top),Math.round(r.right),Math.round(r.bottom)];})`); check('trigger sits beside the guide button', await ev(`(()=>{const g=document.querySelector('.landing-guide-trigger').getBoundingClientRect(),v=document.querySelector('.landing-video-trigger').getBoundingClientRect();return v.width>0&&Math.abs(g.top-v.top)<1&&Math.abs(g.height-v.height)<1&&v.left>g.right;})()`)); check('video is not downloaded before opening', await ev(mp4Requests) === 0 && await ev(`!document.querySelector('#landing-video video source')`)); await shot('01-landing-1600'); await ev(`document.querySelector('.landing-video-trigger').click()`); check('modal opens with focus on close button', await ev(`document.getElementById('landing-video').open&&document.activeElement.classList.contains('landing-video-close')`)); await until(`document.querySelector('#landing-video video').readyState>=3`, 200); await delay(2200); const media = await ev(`(()=>{const v=document.querySelector('#landing-video video');return{t:v.currentTime,d:v.duration,w:v.videoWidth,h:v.videoHeight,paused:v.paused,muted:v.muted,src:v.currentSrc.split('/').pop()};})()`); report.metrics.media = media; check('mp4 loads at 1920x1080 for about 56 s', media.w === 1920 && media.h === 1080 && Math.abs(media.d - 56) < 1); check('video autoplays muted after opening', !media.paused && media.muted && media.t > 1); check('modal fits the viewport', await ev(`(()=>{const r=document.getElementById('landing-video').getBoundingClientRect();return r.left>=0&&r.top>=0&&r.right<=innerWidth&&r.bottom<=innerHeight;})()`)); await shot('02-playing-1600'); await pressEsc(); await delay(200); check('Esc closes, pauses, rewinds and restores focus', await ev(`(()=>{const v=document.querySelector('#landing-video video');return !document.getElementById('landing-video').open&&v.paused&&v.currentTime===0&&document.activeElement.classList.contains('landing-video-trigger');})()`)); await ev(`document.querySelector('.landing-video-trigger').click()`); await delay(300); await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: 8, y: 8, button: 'left', clickCount: 1 }); await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: 8, y: 8, button: 'left', clickCount: 1 }); await delay(200); check('backdrop click closes the modal', await ev(`!document.getElementById('landing-video').open`)); await ev(`document.querySelector('.landing-video-trigger').click()`); await delay(300); await ev(`document.querySelector('.landing-video-close').click()`); await delay(200); check('close button closes and pauses', await ev(`!document.getElementById('landing-video').open&&document.querySelector('#landing-video video').paused`)); for (const [w, h, mobile] of [[1280, 800, false], [1920, 1080, false], [390, 844, true]]) { await viewport(w, h, mobile); await load(); check(`${w}px trigger visible without horizontal scroll`, await ev(`(()=>{const r=document.querySelector('.landing-video-trigger').getBoundingClientRect();return r.width>0&&r.right<=innerWidth&&document.documentElement.scrollWidth<=innerWidth;})()`)); await ev(`document.querySelector('.landing-video-trigger').click()`); await delay(500); check(`${w}px modal fits viewport`, await ev(`(()=>{const r=document.getElementById('landing-video').getBoundingClientRect();return r.left>=0&&r.right<=innerWidth&&r.bottom<=innerHeight+1;})()`)); await shot(`03-modal-${w}`); await ev(`document.querySelector('.landing-video-close').click()`); } await viewport(1600, 1000); await send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] }); await load(); await ev(`document.querySelector('.landing-video-trigger').click()`); await delay(1500); check('reduced motion: opens without autoplay', await ev(`document.getElementById('landing-video').open&&document.querySelector('#landing-video video').paused`)); await send('Emulation.setEmulatedMedia', { features: [] }); check('no runtime exceptions', report.errors.length === 0); report.result = 'PASS'; })().catch(e => { report.result = 'FAIL'; report.failure = e.stack; process.exitCode = 1; }).finally(() => { fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify({ result: report.result, failure: report.failure, metrics: report.metrics, errors: report.errors })); ws?.close(); });