mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-09 13:17:52 +00:00
373 lines
14 KiB
JavaScript
373 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
/* global window, document, innerWidth, innerHeight, getComputedStyle, requestAnimationFrame */
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { 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 { createNativeSession } from '../native-ui/session.mjs';
|
|
import { contentSizes, fileDigest, packageDigest } from '../native-ui/contract.mjs';
|
|
import { maintainedAssets, 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'] = process.argv.slice(2);
|
|
assert(
|
|
appArgument?.endsWith('.app') && outputArgument && mode === 'capture',
|
|
'Usage: node scripts/docs-media/run.mjs <candidate.app> <new-external-directory> [capture]'
|
|
);
|
|
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: [],
|
|
};
|
|
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() {
|
|
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,
|
|
};
|
|
}
|
|
const native = await app.evaluate(async ({ BrowserWindow, screen }) => {
|
|
const win = BrowserWindow.getAllWindows().find((w) => w.isVisible());
|
|
return {
|
|
bounds: win.getBounds(),
|
|
contentBounds: win.getContentBounds(),
|
|
scaleFactor: screen.getDisplayMatching(win.getBounds()).scaleFactor,
|
|
png: (await win.capturePage()).toPNG().toString('base64'),
|
|
};
|
|
});
|
|
assert.equal(native.contentBounds.width, contentSizes.normal.width);
|
|
assert.equal(native.contentBounds.height, contentSizes.normal.height);
|
|
return {
|
|
bytes: Buffer.from(native.png, 'base64'),
|
|
width: contentSizes.normal.width,
|
|
height: contentSizes.normal.height,
|
|
scaleFactor: native.scaleFactor,
|
|
nativeWindow: { bounds: native.bounds, contentBounds: native.contentBounds },
|
|
};
|
|
}
|
|
async function recordAsset(name, captured, method, recording) {
|
|
const file = path.join(output, name);
|
|
report.assets.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,
|
|
method,
|
|
capturedAt: new Date().toISOString(),
|
|
},
|
|
...(recording ? { recording } : {}),
|
|
});
|
|
await persist();
|
|
console.log(`captured: ${name}`);
|
|
}
|
|
async function still(name) {
|
|
await settle();
|
|
const captured = await capture();
|
|
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;
|
|
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();
|
|
await button('Settings').click();
|
|
const settings = page.getByRole('dialog', { name: /^Settings(?: Board Only)?$/ });
|
|
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 button('Close settings').click();
|
|
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();
|
|
await expect(page.getByRole('dialog', { name: /^Settings(?: Board Only)?$/ })).toBeVisible();
|
|
await still('mobile-settings.png');
|
|
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})`
|
|
);
|