mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-08 04:37:51 +00:00
734 lines
30 KiB
JavaScript
734 lines
30 KiB
JavaScript
#!/usr/bin/env node
|
|
/* global 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 { expect } from '@playwright/test';
|
|
import { createNativeSession } from './session.mjs';
|
|
import {
|
|
fileDigest,
|
|
evidenceFailures,
|
|
geometryFailures,
|
|
contentSizes,
|
|
modes,
|
|
packageDigest,
|
|
routes,
|
|
schema,
|
|
settingsSections,
|
|
states,
|
|
seededCases,
|
|
} from './contract.mjs';
|
|
import { installPreviewFixture } from './preview-fixture.mjs';
|
|
import { measurePageHeader, pageHeaderFailures } from './layout-contract.mjs';
|
|
import { performance } from 'node:perf_hooks';
|
|
|
|
const root = path.resolve(import.meta.dirname, '../..');
|
|
const [packageArgument, outputArgument] = process.argv.slice(2);
|
|
assert(
|
|
packageArgument?.endsWith('.app') && outputArgument,
|
|
'Usage: node scripts/native-ui/run.mjs <candidate.app> <new-evidence-directory>'
|
|
);
|
|
assert.equal(process.platform, 'darwin', 'This gate must run on macOS');
|
|
const packagePath = await realpath(packageArgument);
|
|
const output = path.resolve(outputArgument);
|
|
await mkdir(output); // Never overwrite a previous run or its failure artifacts.
|
|
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, 'desktop/package.json'), 'utf8')).version;
|
|
const report = {
|
|
schema,
|
|
boundary: 'packaged-macos',
|
|
status: 'running',
|
|
commit,
|
|
dirty: !!git('status', '--porcelain'),
|
|
version,
|
|
packagePath,
|
|
packageDigest: await packageDigest(packagePath),
|
|
startedAt: new Date().toISOString(),
|
|
entries: [],
|
|
httpFailures: [],
|
|
};
|
|
const persist = () =>
|
|
writeFile(path.join(output, 'evidence.json'), JSON.stringify(report, null, 2) + '\n');
|
|
await persist();
|
|
const session = await createNativeSession({ packagePath, commit, version });
|
|
const { origin } = session;
|
|
let app;
|
|
let page;
|
|
let fixtureTask;
|
|
let activeState = 'startup';
|
|
async function launch() {
|
|
const launched = await session.launch();
|
|
({ app, page } = launched);
|
|
report.identity = launched.identity;
|
|
page.on('response', (response) => {
|
|
if (response.status() >= 400) {
|
|
report.httpFailures.push({
|
|
state: activeState,
|
|
path: new URL(response.url()).pathname,
|
|
method: response.request().method(),
|
|
status: response.status(),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
async function resize(width, height) {
|
|
await app.evaluate(
|
|
({ BrowserWindow }, size) => {
|
|
const window = BrowserWindow.getAllWindows().find((w) => w.isVisible());
|
|
window.webContents.setZoomFactor(1);
|
|
window.setContentSize(size.width, size.height);
|
|
},
|
|
{ width, height }
|
|
);
|
|
await expect.poll(() => page.evaluate(() => [innerWidth, innerHeight])).toEqual([width, height]);
|
|
}
|
|
async function configure(mode) {
|
|
await resize(mode.width, mode.height);
|
|
if (await page.getByRole('dialog').count()) await page.goto(origin);
|
|
if ((await page.locator('html').getAttribute('data-mantine-color-scheme')) !== mode.theme)
|
|
await button('Toggle theme').click();
|
|
if (new URL(page.url()).pathname !== '/')
|
|
await page
|
|
.getByRole('complementary', { name: 'Desktop navigation' })
|
|
.getByRole('button', { name: 'Board', exact: true })
|
|
.click();
|
|
await expect(page.locator('html')).toHaveAttribute('data-mantine-color-scheme', mode.theme);
|
|
await expect(page.getByRole('button', { name: 'Settings', exact: true })).toBeVisible();
|
|
}
|
|
async function metrics() {
|
|
const geometry = await page.evaluate(() => {
|
|
const shell = document.querySelector('.desktop-app-shell');
|
|
if (!shell) throw new Error('Missing production desktop shell');
|
|
const rect = (el) => {
|
|
const b = el.getBoundingClientRect();
|
|
return { x: b.x, y: b.y, width: b.width, height: b.height, right: b.right, bottom: b.bottom };
|
|
};
|
|
const paintedOpacity = (el) => {
|
|
let opacity = 1;
|
|
for (let node = el; node; node = node.parentElement) {
|
|
const style = getComputedStyle(node);
|
|
if (style.visibility !== 'visible' || style.display === 'none') return 0;
|
|
opacity *= Number(style.opacity);
|
|
}
|
|
return opacity;
|
|
};
|
|
return {
|
|
width: innerWidth,
|
|
height: innerHeight,
|
|
rem: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
shell: rect(shell),
|
|
overlays: [
|
|
...document.querySelectorAll(
|
|
'[data-overlay-active="true"] .vk-overlay-content, .vk-overlay-content[data-overlay-active="true"], [data-testid="task-detail-panel"][data-overlay-active="true"]'
|
|
),
|
|
].map((el) => ({
|
|
...rect(el),
|
|
compound: !!el.closest('[data-overlay-compound]'),
|
|
opacity: paintedOpacity(el),
|
|
overflow: el.scrollWidth - el.clientWidth,
|
|
parts: [
|
|
...el.querySelectorAll(
|
|
'.vk-overlay-header, .vk-overlay-body, .vk-overlay-footer, .vk-overlay-scroll, .vk-task-workspace-header, .mantine-Drawer-body'
|
|
),
|
|
].map((part) => {
|
|
const style = getComputedStyle(part);
|
|
const b = rect(part);
|
|
return {
|
|
insetKind: part.matches('.vk-task-workspace-header')
|
|
? 'task-header'
|
|
: part.matches('.mantine-Drawer-body')
|
|
? 'task-body'
|
|
: part.matches('.vk-overlay-scroll')
|
|
? 'scroll'
|
|
: part.matches('.vk-overlay-header')
|
|
? 'header'
|
|
: part.matches('.vk-overlay-footer')
|
|
? 'footer'
|
|
: 'body',
|
|
name: part.matches('.vk-overlay-header, .vk-task-workspace-header')
|
|
? 'header'
|
|
: part.matches('.vk-overlay-footer')
|
|
? 'footer'
|
|
: 'body',
|
|
...b,
|
|
visible:
|
|
b.width > 0 &&
|
|
b.height > 0 &&
|
|
b.x >= 0 &&
|
|
b.y >= 0 &&
|
|
b.right <= innerWidth + 1 &&
|
|
b.bottom <= innerHeight + 1,
|
|
padding: [
|
|
style.paddingTop,
|
|
style.paddingRight,
|
|
style.paddingBottom,
|
|
style.paddingLeft,
|
|
].map(parseFloat),
|
|
};
|
|
}),
|
|
})),
|
|
};
|
|
});
|
|
geometry.primaryHeader = await page.evaluate(measurePageHeader);
|
|
return geometry;
|
|
}
|
|
async function capture(entry) {
|
|
// Native capturePage does not wait for CSS transitions as Playwright screenshots do.
|
|
// Wait for the real fade to complete, then allow the compositor to paint it.
|
|
await expect
|
|
.poll(async () => (await metrics()).overlays.every((overlay) => overlay.opacity >= 0.999))
|
|
.toBe(true);
|
|
await page.evaluate(
|
|
() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
);
|
|
entry.route = new URL(page.url()).pathname;
|
|
entry.theme = await page.locator('html').getAttribute('data-mantine-color-scheme');
|
|
entry.geometry = await metrics();
|
|
const native = await app.evaluate(async ({ BrowserWindow, screen }) => {
|
|
const window = BrowserWindow.getAllWindows().find((w) => w.isVisible());
|
|
return {
|
|
bounds: window.getBounds(),
|
|
contentBounds: window.getContentBounds(),
|
|
minimumSize: window.getMinimumSize(),
|
|
scaleFactor: screen.getDisplayMatching(window.getBounds()).scaleFactor,
|
|
png: (await window.capturePage()).toPNG().toString('base64'),
|
|
};
|
|
});
|
|
const name = `${entry.id.replaceAll('/', '--')}.png`;
|
|
await writeFile(path.join(output, name), Buffer.from(native.png, 'base64'));
|
|
delete native.png;
|
|
entry.nativeWindow = native;
|
|
entry.screenshot = { path: name, sha256: await fileDigest(path.join(output, name)) };
|
|
assert.deepEqual(geometryFailures(entry.geometry), [], entry.id);
|
|
const route = routes.find(([name]) => entry.id.endsWith(`/route-${name}`));
|
|
if (route)
|
|
assert.deepEqual(
|
|
pageHeaderFailures(
|
|
entry.geometry.primaryHeader,
|
|
entry.geometry.rem,
|
|
route[2],
|
|
entry.geometry
|
|
),
|
|
[],
|
|
entry.id
|
|
);
|
|
}
|
|
const button = (name) => page.getByRole('button', { name, exact: true });
|
|
async function dismiss(dialog, opener) {
|
|
await page.keyboard.press('Escape');
|
|
await expect(dialog).toBeHidden();
|
|
if (opener) await expect(opener).toBeFocused();
|
|
}
|
|
async function createTask(title) {
|
|
await button('New Task').click();
|
|
const dialog = page.getByRole('dialog', { name: 'Create New Task', exact: true });
|
|
await dialog.getByRole('textbox', { name: 'Title', exact: true }).fill(title);
|
|
await dialog
|
|
.getByLabel('Description', { exact: true })
|
|
.fill('Public-safe native UI acceptance fixture.');
|
|
const response = page.waitForResponse(
|
|
(r) => new URL(r.url()).pathname === '/api/tasks' && r.request().method() === 'POST'
|
|
);
|
|
await dialog.getByRole('button', { name: 'Create Task', exact: true }).click();
|
|
const result = await response;
|
|
assert(result.ok(), 'Synthetic task creation failed');
|
|
await expect(dialog).toBeHidden();
|
|
const body = await result.json();
|
|
return body.data ?? body;
|
|
}
|
|
async function openTask() {
|
|
if (!fixtureTask) fixtureTask = await createTask('Native public-safe fixture');
|
|
await page.locator(`[data-task-id="${fixtureTask.id}"]`).click();
|
|
const panel = page.getByTestId('task-detail-panel');
|
|
await expect(panel).toBeVisible();
|
|
return panel;
|
|
}
|
|
async function exercise(state, mode, shot) {
|
|
if (state === 'board') {
|
|
await expect(page.locator('html')).toHaveAttribute('data-client', 'desktop');
|
|
await shot();
|
|
} else if (state.startsWith('route-')) {
|
|
const [, route, title] = routes.find(([id]) => state === `route-${id}`);
|
|
// Use the actual navigation button so route transition and Back are exercised.
|
|
const navLabel =
|
|
{
|
|
'Task Templates': 'Templates',
|
|
'Behavioral Drift Monitor': 'Drift Monitor',
|
|
'Agent Output Scoring': 'Scoring',
|
|
'Agent Policies': 'Policies',
|
|
}[title] ?? title;
|
|
await page
|
|
.getByRole('complementary', { name: 'Desktop navigation' })
|
|
.getByRole('button', { name: navLabel, exact: true })
|
|
.click();
|
|
await expect(page).toHaveURL(`${origin}${route}`);
|
|
const shell = page.locator('[data-page-shell="primary"]');
|
|
await expect(shell.getByRole('heading', { name: title, level: 1, exact: true })).toBeFocused();
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1);
|
|
await expect(button('Expand right sidebar').or(button('Collapse right sidebar'))).toHaveCount(
|
|
0
|
|
);
|
|
const back = shell.getByRole('button', { name: 'Back', exact: true });
|
|
await expect(back).toHaveText('');
|
|
await shot();
|
|
await back.click();
|
|
await expect(page).toHaveURL(`${origin}/`);
|
|
} else if (state.startsWith('settings-')) {
|
|
const tab = settingsSections.find((label) => state === `settings-${label.toLowerCase()}`);
|
|
const opener = button('Settings');
|
|
await opener.click();
|
|
const dialog = page.locator('.settings-dialog-content');
|
|
await dialog.getByRole('tab', { name: tab, exact: true }).click();
|
|
await expect(dialog.getByRole('tab', { name: tab, exact: true })).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
);
|
|
await shot();
|
|
await dismiss(dialog, opener);
|
|
} else if (state === 'left-rail' || state === 'right-rail') {
|
|
const side = state === 'left-rail' ? 'left' : 'right';
|
|
const expand = button(`Expand ${side} sidebar`);
|
|
if (await expand.isVisible()) await expand.click();
|
|
const collapse = button(`Collapse ${side} sidebar`);
|
|
await expect(collapse).toHaveAttribute('aria-expanded', 'true');
|
|
const controlled = page.locator(`#${await collapse.getAttribute('aria-controls')}`);
|
|
await expect(controlled).toBeVisible();
|
|
await shot();
|
|
await collapse.click();
|
|
await expect(expand).toHaveAttribute('aria-expanded', 'false');
|
|
} else if (state === 'board-chat' || state === 'squad-chat') {
|
|
const channel = state === 'board-chat' ? 'Board Chat' : 'Squad Chat';
|
|
await button(`Open ${channel}`).click();
|
|
const dock = page.getByRole('region', { name: 'Workbench right dock' });
|
|
await expect(dock).toBeVisible();
|
|
if (state === 'squad-chat') {
|
|
const sender = dock.getByRole('combobox', { name: 'Sending as' });
|
|
await sender.click();
|
|
await expect(page.getByRole('listbox', { name: 'Sending as' })).toBeInViewport();
|
|
await sender.press('Escape');
|
|
await expect(sender).toBeFocused();
|
|
}
|
|
await shot();
|
|
await button(`Close ${channel}`).click();
|
|
await expect(dock).toBeHidden();
|
|
} else if (['task-drawer', 'task-expanded', 'task-chat'].includes(state)) {
|
|
const panel = await openTask();
|
|
if (state === 'task-expanded')
|
|
await panel.getByRole('button', { name: 'Expand task workspace', exact: true }).click();
|
|
if (state === 'task-chat') {
|
|
await panel.getByRole('button', { name: 'Chat', exact: true }).first().click();
|
|
const chat = panel.getByRole('region', { name: /^Task Chat:/ });
|
|
await chat
|
|
.getByRole('textbox', { name: 'Message Task Chat' })
|
|
.fill('Unsent public-safe draft');
|
|
await shot();
|
|
await chat.getByRole('button', { name: 'Close Task Chat panel', exact: true }).click();
|
|
await expect(chat).toBeHidden();
|
|
} else await shot();
|
|
await panel.getByRole('button', { name: 'Close task workspace', exact: true }).click();
|
|
await expect(panel).toBeHidden();
|
|
} else if (state === 'preview') {
|
|
if (!fixtureTask) fixtureTask = await createTask('Native public-safe fixture');
|
|
const removeFixture = await installPreviewFixture(page, fixtureTask.id);
|
|
try {
|
|
const panel = await openTask();
|
|
await panel.getByRole('button', { name: 'Results', exact: true }).click();
|
|
const opener = panel.getByRole('button', { name: 'Preview acceptance.txt', exact: true });
|
|
await opener.click();
|
|
const dialog = page.getByRole('dialog', {
|
|
name: 'Preview: Native acceptance artifact',
|
|
exact: true,
|
|
});
|
|
await expect(dialog).toContainText('Native text evidence');
|
|
await expect(dialog.getByRole('button', { name: 'Download', exact: true })).toBeInViewport({
|
|
ratio: 1,
|
|
});
|
|
await shot({ fixtureBoundary: 'read-only text artifact responses' });
|
|
await dismiss(dialog, opener);
|
|
await panel.getByRole('button', { name: 'Close task workspace', exact: true }).click();
|
|
} finally {
|
|
await removeFixture();
|
|
}
|
|
} else if (state === 'create-template' || state === 'edit-template') {
|
|
await page.goto(`${origin}/templates`);
|
|
const name = `Native template ${mode.id}`;
|
|
if (state === 'edit-template') {
|
|
await page.getByPlaceholder('Search templates...').fill(name);
|
|
await button('Edit').click();
|
|
} else await button('New Template').click();
|
|
const dialog = page.getByRole('dialog', {
|
|
name: state === 'edit-template' ? 'Edit Template' : 'Create New Template',
|
|
exact: true,
|
|
});
|
|
await expect(dialog.getByRole('textbox', { name: 'Template Name', exact: true })).toBeFocused();
|
|
await dialog.getByRole('textbox', { name: 'Template Name', exact: true }).fill(name);
|
|
const text = `# Native acceptance\n\n${'Public-safe description.\n'.repeat(40)}${state}`;
|
|
await dialog.getByLabel('Description Template', { exact: true }).fill(text);
|
|
const scroll = dialog.getByTestId('template-editor-scroll-region');
|
|
await scroll.hover();
|
|
await page.mouse.wheel(0, 900);
|
|
const save = dialog.getByRole('button', {
|
|
name: state === 'edit-template' ? 'Update Template' : 'Create Template',
|
|
exact: true,
|
|
});
|
|
await expect(save).toBeInViewport({ ratio: 1 });
|
|
await shot();
|
|
await save.click();
|
|
await expect(dialog).toBeHidden();
|
|
await page.reload();
|
|
await page.getByPlaceholder('Search templates...').fill(name);
|
|
await button('Edit').click();
|
|
const edited = page.getByRole('dialog', { name: 'Edit Template', exact: true });
|
|
await expect(edited.getByLabel('Description Template', { exact: true })).toHaveValue(text);
|
|
await edited.getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
await expect(edited).toBeHidden();
|
|
} else if (state === 'header-controls') {
|
|
const header = page.getByRole('navigation', { name: 'Main navigation', exact: true });
|
|
const controls = await header
|
|
.locator('button:visible, input[role="combobox"]:visible')
|
|
.evaluateAll((elements) =>
|
|
elements.map((el) => ({
|
|
name: el.getAttribute('aria-label') || el.textContent.trim(),
|
|
role: el.getAttribute('role'),
|
|
}))
|
|
);
|
|
const exercised = [];
|
|
for (const { name, role } of controls) {
|
|
const control =
|
|
role === 'combobox'
|
|
? header.getByRole('combobox', { name, exact: true })
|
|
: header.getByRole('button', { name, exact: true });
|
|
if (name === 'Refresh page') {
|
|
await Promise.all([page.waitForEvent('load'), control.click()]);
|
|
await expect(button('Settings')).toBeVisible();
|
|
} else if (/^(Expand|Collapse) (left|right) sidebar$/.test(name)) {
|
|
const before = await control.getAttribute('aria-expanded');
|
|
await control.click();
|
|
const side = name.includes('left') ? 'left' : 'right';
|
|
await expect(
|
|
button(`${before === 'true' ? 'Expand' : 'Collapse'} ${side} sidebar`)
|
|
).toHaveAttribute('aria-expanded', before === 'true' ? 'false' : 'true');
|
|
} else if (
|
|
name === 'New Task' ||
|
|
name === 'Settings' ||
|
|
name === 'Search' ||
|
|
name === 'Command palette'
|
|
) {
|
|
await control.click();
|
|
const dialog = page.getByRole('dialog', {
|
|
name: {
|
|
'New Task': 'Create New Task',
|
|
Settings: /^Settings(?: Board Only)?$/,
|
|
Search: 'Universal Search',
|
|
'Command palette': 'Command palette',
|
|
}[name],
|
|
exact: true,
|
|
});
|
|
await expect(dialog).toBeVisible();
|
|
await dismiss(dialog);
|
|
} else if (/^(Open|Close|Switch to) (Board|Squad) Chat$/.test(name)) {
|
|
await control.click();
|
|
const dock = page.getByRole('region', { name: 'Workbench right dock' });
|
|
await expect(dock).toBeVisible();
|
|
await button(name.includes('Squad') ? 'Close Squad Chat' : 'Close Board Chat').click();
|
|
await expect(dock).toBeHidden();
|
|
} else if (name === 'Toggle theme') {
|
|
await control.click();
|
|
await expect(page.locator('html')).toHaveAttribute(
|
|
'data-mantine-color-scheme',
|
|
mode.theme === 'light' ? 'dark' : 'light'
|
|
);
|
|
await control.click();
|
|
} else if (name === 'Workspace') {
|
|
await control.click();
|
|
await expect(page.getByRole('listbox', { name: 'Workspace' })).toBeVisible();
|
|
await control.press('Escape');
|
|
} else if (name === 'More views') {
|
|
await control.click();
|
|
await expect(page.getByRole('menu')).toBeVisible();
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.getByRole('menu')).toBeHidden();
|
|
} else if (name === 'Session menu') {
|
|
await control.click();
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.getByRole('dialog')).toBeHidden();
|
|
} else if (/^WebSocket /.test(name)) {
|
|
await control.click();
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
await page.keyboard.press('Escape');
|
|
} else throw new Error(`Visible header control has no behavioral assertion: ${name}`);
|
|
exercised.push(name);
|
|
}
|
|
assert.equal(exercised.length, controls.length);
|
|
await shot({ headerControls: exercised });
|
|
} else if (state === 'create-task') {
|
|
const opener = button('New Task');
|
|
await opener.click();
|
|
const dialog = page.getByRole('dialog', { name: 'Create New Task', exact: true });
|
|
await expect(dialog.getByRole('textbox', { name: 'Title', exact: true })).toBeFocused();
|
|
await dialog
|
|
.getByRole('textbox', { name: 'Title', exact: true })
|
|
.fill(`Native acceptance ${mode.id}`);
|
|
await dialog
|
|
.getByLabel('Description', { exact: true })
|
|
.fill('Public-safe native UI acceptance task.');
|
|
await shot();
|
|
await dialog.getByRole('button', { name: 'Create Task', exact: true }).click();
|
|
await expect(dialog).toBeHidden();
|
|
await expect(
|
|
page.getByRole('heading', { name: `Native acceptance ${mode.id}`, exact: true })
|
|
).toBeVisible();
|
|
} else if (state === 'confirmation') {
|
|
await button('Settings').click();
|
|
const settings = page.locator('.settings-dialog-content');
|
|
const reset = settings.getByRole('button', { name: 'Reset All', exact: true });
|
|
await reset.click();
|
|
const dialog = page.getByRole('dialog', { name: 'Reset all settings?', exact: true });
|
|
await expect(dialog.getByRole('button', { name: 'Cancel', exact: true })).toBeFocused();
|
|
await shot();
|
|
await dismiss(dialog, reset);
|
|
await dismiss(settings, button('Settings'));
|
|
} else if (state === 'search' || state === 'command-palette') {
|
|
const palette = state === 'command-palette';
|
|
if (palette) await page.keyboard.press('Meta+k');
|
|
else await button('Search').click();
|
|
const dialog = page.getByRole('dialog', {
|
|
name: palette ? 'Command palette' : 'Universal Search',
|
|
exact: true,
|
|
});
|
|
await expect(
|
|
dialog.getByRole('textbox', {
|
|
name: palette ? 'Search commands' : 'Search Veritas',
|
|
exact: true,
|
|
})
|
|
).toBeFocused();
|
|
await shot();
|
|
await dismiss(dialog);
|
|
} else if (state === 'responsive-collapse') {
|
|
await resize(contentSizes.normal.width, contentSizes.normal.height);
|
|
if (await button('Expand left sidebar').isVisible())
|
|
await button('Expand left sidebar').click();
|
|
if (await button('Expand right sidebar').isVisible())
|
|
await button('Expand right sidebar').click();
|
|
await button('Open Board Chat').click();
|
|
await resize(contentSizes.minimum.width, contentSizes.minimum.height);
|
|
await expect(page.getByRole('region', { name: 'Workbench right dock' })).toBeHidden();
|
|
await expect(button('Expand left sidebar')).toBeVisible();
|
|
await expect(button('Expand right sidebar')).toBeVisible();
|
|
await resize(mode.width, mode.height);
|
|
await shot();
|
|
} else if (state === 'relaunch') {
|
|
await app.close();
|
|
app = undefined;
|
|
await launch();
|
|
await expect
|
|
.poll(() => page.evaluate(() => [innerWidth, innerHeight]))
|
|
.toEqual([mode.width, mode.height]);
|
|
await expect(page.locator('html')).toHaveAttribute('data-mantine-color-scheme', mode.theme);
|
|
await expect(
|
|
page.getByRole('heading', { name: `Native acceptance ${mode.id}`, exact: true })
|
|
).toBeVisible();
|
|
await shot();
|
|
} else {
|
|
throw new Error(`Required scenario has not been implemented: ${state}`);
|
|
}
|
|
}
|
|
async function checkSeededRendererFailures() {
|
|
report.seededFailures = [];
|
|
for (const [id, expectedFailure] of seededCases) {
|
|
const entry = { id: `seed/${id}`, status: 'running' };
|
|
activeState = entry.id;
|
|
report.seededFailures.push(entry);
|
|
try {
|
|
await configure(modes[0]);
|
|
if (id === 'heading-offset' || id === 'context-invalid-header') {
|
|
await page
|
|
.getByRole('complementary', { name: 'Desktop navigation' })
|
|
.getByRole('button', { name: 'Activity', exact: true })
|
|
.click();
|
|
await expect(page.getByRole('heading', { name: 'Activity', level: 1 })).toBeFocused();
|
|
}
|
|
if (id === 'clipped-modal' || id === 'overlay-padding') {
|
|
await button('New Task').click();
|
|
await expect(page.getByRole('dialog', { name: 'Create New Task' })).toBeVisible();
|
|
}
|
|
await page.evaluate((fault) => {
|
|
if (fault === 'shell-blank')
|
|
document
|
|
.querySelector('.desktop-app-shell')
|
|
.style.setProperty('height', `${innerHeight - 100}px`, 'important');
|
|
else if (fault === 'heading-offset')
|
|
document.querySelector('[data-page-shell="primary"] h1').style.transform =
|
|
'translateY(16px)';
|
|
else if (fault === 'context-invalid-header') {
|
|
const button = document.createElement('button');
|
|
button.setAttribute('aria-label', 'Expand right sidebar');
|
|
button.textContent = 'Invalid board control';
|
|
document.querySelector('[aria-label="Main navigation"]').append(button);
|
|
} else if (fault === 'clipped-modal')
|
|
document
|
|
.querySelector('[data-overlay-active="true"] .vk-overlay-content')
|
|
.style.setProperty('transform', `translateX(${innerWidth}px)`, 'important');
|
|
else if (fault === 'overlay-padding')
|
|
document
|
|
.querySelector('[data-overlay-active="true"] .vk-overlay-header')
|
|
.style.setProperty('padding', '0px', 'important');
|
|
else if (fault === 'dead-header') {
|
|
const button = [
|
|
...document.querySelectorAll('[aria-label="Main navigation"] button'),
|
|
].find((element) => element.textContent.trim() === 'New Task');
|
|
button.replaceWith(button.cloneNode(true)); // Remove React's target handler, retain visible UI.
|
|
}
|
|
}, id);
|
|
entry.observedFailures = [];
|
|
if (id === 'dead-header') {
|
|
entry.behavior = {
|
|
control: 'New Task',
|
|
visibleBefore: await button('New Task').isVisible(),
|
|
enabledBefore: await button('New Task').isEnabled(),
|
|
dialogVisibleBefore: await page
|
|
.getByRole('dialog', { name: 'Create New Task' })
|
|
.isVisible(),
|
|
};
|
|
await button('New Task').click();
|
|
entry.behavior.clickCompleted = true;
|
|
const started = performance.now();
|
|
try {
|
|
await expect(page.getByRole('dialog', { name: 'Create New Task' })).toBeVisible({
|
|
timeout: 1000,
|
|
});
|
|
} catch (error) {
|
|
if (!error.message.includes('toBeVisible')) throw error;
|
|
entry.observedFailures.push('dead header control');
|
|
}
|
|
entry.behavior.waitedMs = performance.now() - started;
|
|
entry.behavior.dialogVisibleAfter = await page
|
|
.getByRole('dialog', { name: 'Create New Task' })
|
|
.isVisible();
|
|
}
|
|
try {
|
|
await capture(entry);
|
|
} catch (error) {
|
|
// Geometry faults must retain their native capture, then be identified by the same validator.
|
|
if (!entry.screenshot) throw error;
|
|
entry.captureAssertion = error.message;
|
|
}
|
|
entry.observedFailures.push(...geometryFailures(entry.geometry));
|
|
if (id === 'heading-offset' || id === 'context-invalid-header')
|
|
entry.observedFailures.push(
|
|
...pageHeaderFailures(
|
|
entry.geometry.primaryHeader,
|
|
entry.geometry.rem,
|
|
'Activity',
|
|
entry.geometry
|
|
)
|
|
);
|
|
assert(
|
|
entry.observedFailures.includes(expectedFailure),
|
|
`Injected fault was not detected: ${id}`
|
|
);
|
|
entry.status = 'detected';
|
|
} catch (error) {
|
|
entry.status = 'failed';
|
|
entry.error = error.message;
|
|
} finally {
|
|
// Faults only affect this disposable renderer; reload removes all injected DOM/CSS.
|
|
await page.goto(origin);
|
|
}
|
|
await persist();
|
|
console.log(`${entry.status}: ${entry.id}`);
|
|
}
|
|
}
|
|
try {
|
|
await launch();
|
|
for (const mode of modes) {
|
|
for (const state of states) {
|
|
const entry = { id: `${mode.id}/${state}`, status: 'running' };
|
|
activeState = entry.id;
|
|
report.entries.push(entry);
|
|
let captured = false;
|
|
try {
|
|
await configure(mode);
|
|
await exercise(state, mode, async (details) => {
|
|
Object.assign(entry, details);
|
|
await capture(entry);
|
|
captured = true;
|
|
assert.deepEqual(
|
|
[entry.theme, entry.geometry.width, entry.geometry.height],
|
|
[mode.theme, mode.width, mode.height],
|
|
`${entry.id}: capture mode changed during interaction`
|
|
);
|
|
});
|
|
assert(captured, 'Scenario did not capture native evidence');
|
|
entry.completionGeometry = await metrics();
|
|
assert.deepEqual(
|
|
[entry.completionGeometry.width, entry.completionGeometry.height],
|
|
[mode.width, mode.height],
|
|
`${entry.id}: completion viewport changed during interaction`
|
|
);
|
|
assert.deepEqual(
|
|
geometryFailures(entry.completionGeometry),
|
|
[],
|
|
`${entry.id}: after interaction`
|
|
);
|
|
assert.equal(
|
|
(report.httpFailures ?? []).filter(
|
|
(failure) =>
|
|
failure.state === entry.id && (failure.status === 429 || failure.status >= 500)
|
|
).length,
|
|
0,
|
|
'Native state encountered rate limiting or a server failure'
|
|
);
|
|
entry.status = 'passed';
|
|
} catch (error) {
|
|
entry.status = 'failed';
|
|
entry.error = error.message;
|
|
if (captured) {
|
|
entry.failure = { id: `${entry.id}-failure` };
|
|
try {
|
|
await capture(entry.failure);
|
|
} catch (captureError) {
|
|
entry.failure.captureError = captureError.message;
|
|
}
|
|
} else {
|
|
try {
|
|
await capture(entry);
|
|
} catch (captureError) {
|
|
entry.captureError = captureError.message;
|
|
}
|
|
}
|
|
}
|
|
console.log(
|
|
`${entry.status}: ${entry.id}${entry.error ? `: ${entry.error.split('\n')[0]}` : ''}`
|
|
);
|
|
await persist();
|
|
}
|
|
}
|
|
await checkSeededRendererFailures();
|
|
report.status = report.entries.every((entry) => entry.status === 'passed') ? 'passed' : 'failed';
|
|
} catch (error) {
|
|
report.status = 'failed';
|
|
report.error = error.message;
|
|
} finally {
|
|
activeState = 'teardown';
|
|
if (app) await app.close();
|
|
report.completedAt = new Date().toISOString();
|
|
report.validationErrors = evidenceFailures(report, {
|
|
commit,
|
|
version,
|
|
packageDigest: report.packageDigest,
|
|
});
|
|
if (report.validationErrors.length) report.status = 'failed';
|
|
await persist();
|
|
}
|
|
console.log(`Native evidence: ${path.join(output, 'evidence.json')} (${report.status})`);
|
|
process.exitCode = report.status === 'passed' ? 0 : 1;
|