#!/usr/bin/env node /* global window, document, innerWidth, innerHeight, getComputedStyle, requestAnimationFrame */ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { copyFile, mkdir, readFile, realpath, writeFile } from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { chromium, expect } from '@playwright/test'; import { nativeSettingsPage, closeNativeSettings } from '../native-ui/settings-window.mjs'; import { createNativeSession } from '../native-ui/session.mjs'; import { contentSizes, fileDigest, packageDigest } from '../native-ui/contract.mjs'; import { maintainedAssets, taskModeAssets, mediaSchema, mediaEvidenceFailures } from './verify.mjs'; import { encodeInteraction, recordInteraction } from './record.mjs'; import { finalizeCapture } from './finalize.mjs'; const root = path.resolve(import.meta.dirname, '../..'); const [appArgument, outputArgument, mode = 'capture', focusedBoardArgument] = process.argv.slice(2); assert( appArgument?.endsWith('.app') && outputArgument && mode === 'capture', 'Usage: node scripts/docs-media/run.mjs [capture] [board-report.json]' ); assert.equal(process.platform, 'darwin', 'Desktop documentation captures require macOS'); const output = path.join( await realpath(path.dirname(path.resolve(outputArgument))), path.basename(outputArgument) ); const canonicalRoot = await realpath(root); assert( !output.startsWith(canonicalRoot + path.sep) && output !== canonicalRoot, 'Capture evidence must remain outside the candidate checkout' ); await mkdir(output); // Refuse to overwrite either successful or failed evidence. const packagePath = await realpath(appArgument); const git = (...args) => { const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr); return result.stdout.trim(); }; const commit = git('rev-parse', 'HEAD'); const version = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')).version; const report = { schema: mediaSchema, status: 'running', mode, commit, version, dirty: git('status', '--porcelain') !== '', packagePath, packageDigest: await packageDigest(packagePath), startedAt: new Date().toISOString(), macOS: os.release(), assets: [], fixture: { source: 'scripts/docs-media/run.mjs', kind: 'synthetic-public-safe', taskTitle: 'Prepare the release candidate', status: 'todo', }, taskModeComparison: { baseline: 'docs/assets/v6.1.7/task-mode-audit/manifest.json', matched: [ 'native window 1180x900', 'dark theme', 'Overview/Plan/Run/Results/History', 'drawer then expanded', ], unmatched: [ 'Original showcase dataset is unavailable; uses the documentation fixture.', 'Native content capture excludes the baseline window frame and shadow.', ], }, }; const persist = () => writeFile(path.join(output, 'evidence.json'), JSON.stringify(report, null, 2) + '\n'); await persist(); let app, browser, page; const session = await createNativeSession({ packagePath, commit, version }); const { origin } = session; const button = (name) => page.getByRole('button', { name, exact: true }); const panel = () => page.getByTestId('task-detail-panel'); const title = 'Prepare the release candidate'; let mobile = false; async function apiRequest(method, pathname, data) { assert.equal(new URL(page.url()).origin, origin); return page.evaluate( async ({ method, pathname, data }) => { const response = await fetch(pathname, { method, credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return { ok: response.ok, status: response.status, body: await response.json() }; }, { method, pathname, data } ); } async function settle() { await page.evaluate(() => document.fonts.ready); await page.evaluate(() => { if (document.getElementById('documentation-capture-caret')) return; const style = document.createElement('style'); style.id = 'documentation-capture-caret'; style.textContent = 'input, textarea, [contenteditable] { caret-color: transparent !important; }'; document.head.append(style); }); await expect .poll(() => page.evaluate(() => [...document.querySelectorAll('[data-overlay-active="true"]')].every( (element) => Number(getComputedStyle(element).opacity) >= 0.999 ) ) ) .toBe(true); await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))) ); } async function capture(taskMode = false) { if (mobile) { assert.deepEqual(await page.evaluate(() => [innerWidth, innerHeight]), [390, 844]); assert.equal(await page.evaluate(() => typeof window.veritasDesktop), 'undefined'); return { bytes: await page.screenshot({ fullPage: false }), width: 390, height: 844, scaleFactor: 1, }; } // DOM readiness and animation frames can precede Electron's last composited frame. // Synchronize the renderer first; the published bytes still come from capturePage. await page.screenshot({ fullPage: false }); const native = await app.evaluate(async ({ BrowserWindow, screen }, url) => { const win = BrowserWindow.getAllWindows().find((w) => w.webContents.getURL() === url); return { bounds: win.getBounds(), contentBounds: win.getContentBounds(), scaleFactor: screen.getDisplayMatching(win.getBounds()).scaleFactor, png: (await win.capturePage()).toPNG().toString('base64'), }; }, page.url()); if (taskMode) { assert.equal(native.bounds.width, 1180); assert.equal(native.bounds.height, 900); assert.equal(native.contentBounds.width, 1180); assert(native.contentBounds.height >= 760 && native.contentBounds.height <= 900); } else { assert.equal(native.contentBounds.width, contentSizes.normal.width); assert.equal(native.contentBounds.height, contentSizes.normal.height); } return { bytes: Buffer.from(native.png, 'base64'), width: native.contentBounds.width, height: native.contentBounds.height, scaleFactor: native.scaleFactor, nativeWindow: { bounds: native.bounds, contentBounds: native.contentBounds }, }; } async function recordAsset(name, captured, method, recording) { const file = path.join(output, name); const taskMode = taskModeAssets.includes(name); const destination = taskMode ? (report.taskModeAssets ??= []) : report.assets; destination.push({ name, decision: 'replace', reason: 'Recaptured the converged interface from the candidate', path: `docs/assets/v${version}/${name}`, sha256: await fileDigest(file), capture: { commit, version, packageDigest: report.packageDigest, boundary: mobile ? 'mobile-browser' : 'packaged-macos', packaged: !mobile, width: captured.width, height: captured.height, scaleFactor: captured.scaleFactor, nativeWindow: captured.nativeWindow, ...(taskMode ? { framing: 'native-content-without-window-frame' } : {}), method, capturedAt: new Date().toISOString(), }, ...(recording ? { recording } : {}), }); await persist(); console.log(`captured: ${name}`); } async function still(name) { await settle(); const captured = await capture(taskModeAssets.includes(name)); await writeFile(path.join(output, name), captured.bytes, { flag: 'wx' }); await recordAsset(name, captured, 'window-capture'); } async function openTask() { await page.getByRole('heading', { name: title, exact: true }).click(); await expect(panel()).toBeVisible(); } async function workspaceMode(name) { if (mobile) { await page.getByRole('combobox', { name: 'Task workspace mode', exact: true }).click(); await page.getByRole('option', { name, exact: true }).click(); } else await panel() .getByRole('navigation', { name: 'Task workspace modes' }) .getByRole('button', { name, exact: true }) .click(); await expect(panel().locator('#task-workspace-mode-heading')).toHaveText(name); } async function interaction(name) { await settle(); const directory = path.join(output, name.replace('.gif', '-frames')); const stages = [ { label: 'Board before opening task', act: async () => {}, verify: async () => expect(page.getByRole('heading', { name: title, exact: true })).toBeVisible(), }, { label: 'Open task workspace', act: openTask, verify: async () => expect(panel()).toBeVisible(), }, { label: 'Switch to Plan', act: () => workspaceMode('Plan'), verify: async () => expect(panel().locator('#task-workspace-mode-heading')).toHaveText('Plan'), }, { label: 'Return to Overview', act: () => workspaceMode('Overview'), verify: async () => expect(panel().locator('#task-workspace-mode-heading')).toHaveText('Overview'), }, { label: 'Close workspace and return to board', act: () => button('Close task workspace').click(), verify: async () => expect(panel()).toBeHidden(), }, ]; const recording = await recordInteraction({ directory, capture, stages }); encodeInteraction({ directory, output: path.join(output, name), fps: recording.fps, width: mobile ? 390 : 1200, }); if (!mobile) { const video = path.join(output, 'demo-overview.mp4'); encodeInteraction({ directory, output: video, fps: recording.fps, width: 1200, format: 'mp4' }); report.demoVideo = { name: 'demo-overview.mp4', sha256: await fileDigest(video), source: path.basename(directory), }; } await recordAsset(name, await capture(), 'interaction-recording', { path: `${path.basename(directory)}/recording.json`, sha256: await fileDigest(path.join(directory, 'recording.json')), events: recording.events, }); } async function theme() { await page.emulateMedia({ reducedMotion: 'reduce' }); if ((await page.locator('html').getAttribute('data-mantine-color-scheme')) !== 'dark') { const toggle = page.getByRole('button', { name: 'Toggle theme', exact: true }); await toggle.click(); } await expect(page.locator('html')).toHaveAttribute('data-mantine-color-scheme', 'dark'); } try { assert(!report.dirty, 'Capture requires a clean build checkout'); const launched = await session.launch(); ({ app, page } = launched); report.identity = launched.identity; const baseline = JSON.parse( await readFile(path.join(root, report.taskModeComparison.baseline), 'utf8') ); const osMatch = baseline.host.osVersion === launched.identity.osVersion; report.taskModeComparison[osMatch ? 'matched' : 'unmatched'].push( `Host macOS: baseline ${baseline.host.osVersion}; candidate ${launched.identity.osVersion}.` ); await app.evaluate(({ BrowserWindow }, sizes) => { const win = BrowserWindow.getAllWindows().find((w) => w.isVisible()); win.webContents.setZoomFactor(1); win.setContentSize(sizes.normal.width, sizes.normal.height); }, contentSizes); await expect .poll(() => page.evaluate(() => [innerWidth, innerHeight])) .toEqual([contentSizes.normal.width, contentSizes.normal.height]); await theme(); // Use the actual isolated packaged API and its authenticated cookie, no auth bypass. for (const [name, status, type] of [ [title, 'todo', 'code'], ['Refresh the visual tour', 'todo', 'content'], ['Verify workspace behavior', 'in-progress', 'research'], ['Resolve documentation drift', 'blocked', 'content'], ['Complete feature verification', 'done', 'research'], ]) { const response = await apiRequest('POST', '/api/tasks', { title: name, description: 'Public-safe documentation fixture for the release candidate.', type, priority: 'medium', }); assert(response.ok, `Fixture creation failed: ${response.status}`); const body = response.body; const task = body.data ?? body; if (status !== 'todo') { const changed = await apiRequest('PATCH', `/api/tasks/${task.id}`, { status }); assert(changed.ok, `Fixture status update failed: ${changed.status}`); } } await page.reload(); await expect(page.getByRole('button', { name: /^System health: Stable/ })).toBeVisible({ timeout: 30_000, }); await expect(page.getByRole('heading', { name: title, exact: true })).toBeVisible(); await still('board-overview.png'); await interaction('board-to-workspace.gif'); await openTask(); await still('task-workspace.png'); await button('Close task workspace').click(); const boardPage = page; await button('Settings').click(); page = await nativeSettingsPage(app); await app.evaluate( ({ BrowserWindow }, size) => { const window = BrowserWindow.getAllWindows().find( (window) => window.webContents.getURL() === size.url ); window.webContents.setZoomFactor(1); window.setContentSize(size.width, size.height); }, { ...contentSizes.normal, url: page.url() } ); const settings = page.getByRole('main', { name: 'Settings', exact: true }); await expect(settings).toBeVisible(); await expect(settings.getByRole('heading', { name: 'General', exact: true })).toBeVisible(); await still('settings-navigation.png'); for (const [name, file] of [ ['Agents', 'agent-providers.png'], ['Maintenance', 'maintenance-center.png'], ['Notifications', 'notification-adapters.png'], ]) { await settings.getByRole('tab', { name, exact: true }).click(); await expect(settings.getByRole('heading', { name, exact: true })).toBeVisible(); await still(file); } await closeNativeSettings(app, page); page = boardPage; await button('Command palette').click(); await expect(page.getByRole('textbox', { name: 'Search commands' })).toBeVisible(); await still('command-palette.png'); await page.keyboard.press('Escape'); await button('Open Board Chat').click(); await expect(page.getByRole('region', { name: 'Workbench right dock' })).toBeVisible(); await still('workbench-panel.png'); await page .getByRole('region', { name: 'Workbench right dock' }) .getByText('Squad Chat', { exact: true }) .click(); await expect(page.getByRole('region', { name: 'Squad Chat', exact: true })).toBeVisible(); await still('squad-chat.png'); browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true, colorScheme: 'dark', reducedMotion: 'reduce', }); page = await context.newPage(); mobile = true; await page.goto(origin); await session.authenticate(page); await expect(page.getByRole('button', { name: 'Mobile board', exact: true })).toBeVisible(); await button('Mobile board').click(); await expect(page.getByRole('heading', { name: title, exact: true })).toBeVisible(); await still('mobile-board.png'); await interaction('mobile-flow.gif'); await openTask(); await still('mobile-task-workspace.png'); await button('Close task workspace').click(); await button('Mobile settings').click(); const mobileSettings = page.getByRole('dialog', { name: /^Settings(?: Board Only)?$/ }); await expect(mobileSettings).toBeVisible(); await expect(mobileSettings.getByRole('heading', { name: 'General', exact: true })).toBeVisible(); await expect(mobileSettings.getByRole('combobox', { name: 'Appearance', exact: true })).toBeVisible(); await still('mobile-settings.png'); page = boardPage; mobile = false; await openTask(); // Match the retained task-mode audit's native window dimensions. Native // capturePage excludes its frame/shadow; that framing difference is recorded. await app.evaluate(({ BrowserWindow }, url) => { const win = BrowserWindow.getAllWindows().find((w) => w.webContents.getURL() === url); win.setSize(1180, 900); }, page.url()); for (const presentation of ['drawer', 'expanded']) { if (presentation === 'expanded') await button('Expand task workspace').click(); for (const name of ['Overview', 'Plan', 'Run', 'Results', 'History']) { await workspaceMode(name); await still(`task-${presentation}-${name.toLowerCase()}.png`); } } await app.evaluate(({ BrowserWindow }, url) => { const win = BrowserWindow.getAllWindows().find((w) => w.webContents.getURL() === url); win.setContentSize(1700, 760); }, page.url()); await button('Close task workspace').click(); if (focusedBoardArgument) { const boardReportPath = await realpath(focusedBoardArgument); const board = JSON.parse(await readFile(boardReportPath, 'utf8')); assert.equal(board.status, 'passed', 'Focused board capture requires a passing feature run'); for (const key of ['commit', 'version', 'packageDigest']) assert.equal(board[key], report[key], `Focused board capture has a different ${key}`); const entry = board.entries?.find((item) => item.count === 5000); assert.deepEqual(entry?.failures, [], 'Focused board capture missed a performance budget'); assert.equal(entry?.screenshot?.name, 'board-5000.png'); const source = path.join(path.dirname(boardReportPath), entry.screenshot.name); assert.equal(await realpath(source), source, 'Focused capture traverses a symlink'); assert.equal( await fileDigest(source), entry.screenshot.sha256, 'Focused capture bytes changed' ); await copyFile(source, path.join(output, entry.screenshot.name)); report.focusedBoardAssets = [ { ...entry.screenshot, decision: 'replace', reason: 'Focused comparison of the matched 5000-task board fixture.', path: `docs/assets/v${version}/${entry.screenshot.name}`, }, ]; } report.assets.sort((a, b) => maintainedAssets.indexOf(a.name) - maintainedAssets.indexOf(b.name)); report.completedAt = new Date().toISOString(); report.status = 'captured'; assert.deepEqual(mediaEvidenceFailures(report, report), []); } catch (error) { report.status = 'failed'; report.error = error.message; process.exitCode = 1; console.error(error.message); } finally { const failures = await finalizeCapture({ report, persist, resources: [ [ 'mobile browser', async () => { if (browser) await browser.close(); }, ], [ 'native app', async () => { if (app) await app.close(); }, ], ], }); if (failures.length) process.exitCode = 1; } console.log( `Documentation capture: ${path.join(output, 'evidence.json')} (${report.status}; ${mode})` );