Align route ownership and sidebar surface exposure

This commit is contained in:
master
2026-03-10 15:32:34 +02:00
parent 5c10aa7f71
commit 72746e2f7b
17 changed files with 687 additions and 90 deletions

View File

@@ -0,0 +1,306 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs';
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputDir = path.join(webRoot, 'output', 'playwright');
const outputPath = path.join(outputDir, 'live-route-surface-ownership-check.json');
const authStatePath = path.join(outputDir, 'live-route-surface-ownership-check.state.json');
const authReportPath = path.join(outputDir, 'live-route-surface-ownership-check.auth.json');
const scopeQuery = 'tenant=demo-prod&regions=us-east&environments=stage&timeWindow=7d';
function isStaticAsset(url) {
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url);
}
function createRuntime() {
return {
consoleErrors: [],
pageErrors: [],
requestFailures: [],
responseErrors: [],
};
}
function attachRuntimeObservers(page, runtime) {
page.on('console', (message) => {
if (message.type() === 'error') {
runtime.consoleErrors.push({ page: page.url(), text: message.text() });
}
});
page.on('pageerror', (error) => {
if (page.url() === 'about:blank' && String(error).includes('sessionStorage')) {
return;
}
runtime.pageErrors.push({
page: page.url(),
text: error instanceof Error ? error.message : String(error),
});
});
page.on('requestfailed', (request) => {
if (isStaticAsset(request.url())) {
return;
}
const errorText = request.failure()?.errorText ?? 'unknown';
if (errorText === 'net::ERR_ABORTED') {
return;
}
runtime.requestFailures.push({
page: page.url(),
method: request.method(),
url: request.url(),
error: errorText,
});
});
page.on('response', (response) => {
if (isStaticAsset(response.url())) {
return;
}
if (response.status() >= 400) {
runtime.responseErrors.push({
page: page.url(),
method: response.request().method(),
status: response.status(),
url: response.url(),
});
}
});
}
async function settle(page) {
await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
await page.waitForTimeout(1_000);
}
async function headingText(page) {
const headings = page.locator('h1, h2, [data-testid="page-title"], .page-title');
const count = await headings.count();
for (let index = 0; index < Math.min(count, 4); index += 1) {
const text = (await headings.nth(index).innerText().catch(() => '')).trim();
if (text) {
return text;
}
}
return '';
}
async function captureSnapshot(page, label) {
return {
label,
url: page.url(),
title: await page.title(),
heading: await headingText(page),
};
}
async function persistSummary(summary) {
summary.lastUpdatedAtUtc = new Date().toISOString();
await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
}
async function runRouteCheck(page, routeCheck) {
const evaluate = async () => {
const currentUrl = new URL(page.url());
const title = await page.title();
const heading = await headingText(page);
let ok = currentUrl.pathname === routeCheck.expectedPath;
if (ok && routeCheck.expectedTitle && !routeCheck.expectedTitle.test(title)) {
ok = false;
}
if (ok && routeCheck.expectedHeading && !routeCheck.expectedHeading.test(heading)) {
ok = false;
}
return {
ok,
finalUrl: page.url(),
snapshot: await captureSnapshot(page, `route:${routeCheck.path}`),
};
};
await page.goto(`https://stella-ops.local${routeCheck.path}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await settle(page);
let result = await evaluate();
if (!result.ok && new URL(result.finalUrl).pathname === new URL(`https://stella-ops.local${routeCheck.path}`).pathname) {
await page.waitForTimeout(2_000);
result = await evaluate();
}
return {
kind: 'route',
route: routeCheck.path,
...result,
};
}
async function runSidebarCheck(page) {
await page.goto(`https://stella-ops.local/mission-control/board?${scopeQuery}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await settle(page);
const hrefs = await page.locator('aside.sidebar a').evaluateAll((nodes) =>
nodes
.map((node) => node.getAttribute('href'))
.filter((value) => typeof value === 'string'),
);
const required = [
'/releases/health',
'/ops/operations/environments',
'/ops/operations/notifications',
];
const forbidden = [
'/setup/notifications',
'/mission-control/alerts',
'/mission-control/activity',
];
const ok = required.every((href) => hrefs.includes(href))
&& forbidden.every((href) => !hrefs.includes(href));
return {
kind: 'sidebar',
route: `/mission-control/board?${scopeQuery}`,
ok,
hrefs,
snapshot: await captureSnapshot(page, 'sidebar:mission-control-board'),
};
}
async function runWatchlistLabelCheck(page, returnTo, expectedLabel) {
await page.goto(
`https://stella-ops.local/setup/trust-signing/watchlist/alerts?${scopeQuery}&alertId=alert-001&scope=tenant&tab=alerts&returnTo=${encodeURIComponent(returnTo)}`,
{
waitUntil: 'domcontentloaded',
timeout: 30_000,
},
);
await settle(page);
const labelVisible = await page.getByText(`Return to ${expectedLabel}`, { exact: false }).first().isVisible().catch(() => false);
return {
kind: 'watchlist-return',
route: page.url(),
ok: labelVisible,
expectedLabel,
snapshot: await captureSnapshot(page, `watchlist-return:${expectedLabel}`),
};
}
async function main() {
await mkdir(outputDir, { recursive: true });
const authReport = await authenticateFrontdoor({
statePath: authStatePath,
reportPath: authReportPath,
});
const browser = await chromium.launch({
headless: true,
args: ['--ignore-certificate-errors', '--disable-dev-shm-usage'],
});
const context = await createAuthenticatedContext(browser, authReport, {
statePath: authStatePath,
});
const runtime = createRuntime();
context.on('page', (page) => attachRuntimeObservers(page, runtime));
const page = await context.newPage();
attachRuntimeObservers(page, runtime);
const summary = {
generatedAtUtc: new Date().toISOString(),
results: [],
runtime,
};
const routeChecks = [
{
path: `/releases/health?${scopeQuery}`,
expectedPath: '/releases/health',
expectedTitle: /release health/i,
},
{
path: `/releases/environments?${scopeQuery}`,
expectedPath: '/ops/operations/environments',
expectedTitle: /environments/i,
},
{
path: `/release-control/environments?${scopeQuery}`,
expectedPath: '/ops/operations/environments',
expectedTitle: /environments/i,
},
{
path: `/setup/notifications?${scopeQuery}`,
expectedPath: '/ops/operations/notifications',
expectedTitle: /notifications/i,
},
{
path: `/ops/operations/notifications?${scopeQuery}`,
expectedPath: '/ops/operations/notifications',
expectedTitle: /notifications/i,
},
];
for (const routeCheck of routeChecks) {
summary.results.push(await runRouteCheck(page, routeCheck));
await persistSummary(summary);
}
summary.results.push(await runSidebarCheck(page));
await persistSummary(summary);
summary.results.push(await runWatchlistLabelCheck(page, '/mission-control/alerts', 'Mission Alerts'));
await persistSummary(summary);
summary.results.push(await runWatchlistLabelCheck(page, '/mission-control/board', 'Dashboard'));
await persistSummary(summary);
await context.close();
await browser.close();
const failedActionCount = summary.results.filter((entry) => !entry.ok).length;
const runtimeIssueCount = runtime.consoleErrors.length
+ runtime.pageErrors.length
+ runtime.requestFailures.length
+ runtime.responseErrors.length;
summary.failedActionCount = failedActionCount;
summary.runtimeIssueCount = runtimeIssueCount;
await persistSummary(summary);
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
if (failedActionCount > 0 || runtimeIssueCount > 0) {
process.exit(1);
}
}
main().catch((error) => {
process.stderr.write(`[live-route-surface-ownership-check] ${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
});