Files
git.stella-ops.org/src/Web/StellaOps.Web/scripts/live-triage-artifacts-scope-compat.mjs

219 lines
8.2 KiB
JavaScript

#!/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-triage-artifacts-scope-compat.json');
const authStatePath = path.join(outputDir, 'live-triage-artifacts-scope-compat.state.json');
const authReportPath = path.join(outputDir, 'live-triage-artifacts-scope-compat.auth.json');
const routePath = '/triage/artifacts';
function scopedUrl(route = routePath) {
return `https://stella-ops.local${route}`;
}
async function settle(page, timeoutMs = 1_500) {
await page.waitForLoadState('domcontentloaded', { timeout: 20_000 }).catch(() => {});
await page.waitForTimeout(timeoutMs);
}
async function waitForArtifactsSurface(page) {
await Promise.race([
page.locator('tbody tr').first().waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.getByText('Unable to load artifacts', { exact: true }).waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.getByText('No artifacts match the current lane and filters.', { exact: true }).waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.waitForTimeout(20_000),
]);
}
async function waitForWorkspaceSurface(page) {
await Promise.race([
page.locator('[data-finding-card]').first().waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.getByText('Unable to load findings', { exact: true }).waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.getByText('No findings for this artifact.', { exact: true }).waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}),
page.waitForTimeout(20_000),
]);
}
async function assert(condition, message, details = {}) {
if (!condition) {
throw new Error(`${message}${Object.keys(details).length ? ` ${JSON.stringify(details)}` : ''}`);
}
}
async function run() {
await mkdir(outputDir, { recursive: true });
const authReport = await authenticateFrontdoor({
statePath: authStatePath,
reportPath: authReportPath,
headless: true,
});
const browser = await chromium.launch({
headless: true,
args: ['--disable-dev-shm-usage'],
});
const context = await createAuthenticatedContext(browser, authReport, { statePath: authStatePath });
const page = await context.newPage();
const consoleErrors = [];
const responseErrors = [];
const requestFailures = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
page.on('response', (response) => {
const url = response.url();
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
return;
}
if (url.includes('/connect/authorize')) {
return;
}
if (response.status() >= 400) {
responseErrors.push({
status: response.status(),
method: response.request().method(),
url,
});
}
});
page.on('requestfailed', (request) => {
const url = request.url();
const failure = request.failure()?.errorText ?? 'unknown';
if (failure === 'net::ERR_ABORTED') {
return;
}
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
return;
}
requestFailures.push({
method: request.method(),
url,
error: failure,
});
});
const checks = [];
try {
await page.goto(scopedUrl(), { waitUntil: 'domcontentloaded', timeout: 30_000 });
await waitForArtifactsSurface(page);
await settle(page);
const heading = (await page.locator('h1').first().textContent())?.trim() ?? '';
await assert(heading === 'Artifact workspace', 'Unexpected artifact workspace heading.', { heading });
checks.push({ name: 'artifacts-heading', ok: true, heading });
const loadErrorVisible = await page.getByText('Unable to load artifacts', { exact: true }).isVisible().catch(() => false);
await assert(!loadErrorVisible, 'Artifact workspace still rendered the load error state.');
checks.push({ name: 'artifacts-no-error-state', ok: true });
const initialRowCount = await page.locator('tbody tr').count();
await assert(initialRowCount > 0, 'Artifact workspace did not render any rows.', { initialRowCount });
checks.push({ name: 'artifacts-row-count', ok: true, initialRowCount });
await page.getByRole('button', { name: 'Needs Review' }).click({ timeout: 10_000 });
await settle(page);
await page.getByPlaceholder('Search artifacts or environments...').fill('asset-web-prod');
await settle(page);
const filteredRowCount = await page.locator('tbody tr').count();
await assert(filteredRowCount === 1, 'Needs Review lane search did not isolate asset-web-prod.', { filteredRowCount });
checks.push({ name: 'artifacts-lane-and-search', ok: true, filteredRowCount });
await page.getByRole('button', { name: 'Open workspace' }).first().click({ timeout: 10_000 });
await waitForWorkspaceSurface(page);
await settle(page);
const detailHeading = (await page.locator('h1').first().textContent())?.trim() ?? '';
const detailSubtitle = ((await page.locator('.subtitle').first().textContent()) || '').trim().replace(/\s+/g, ' ');
await assert(detailHeading === 'Artifact triage', 'Unexpected workspace heading after opening an artifact.', {
detailHeading,
url: page.url(),
});
await assert(detailSubtitle.includes('asset-web-prod'), 'Workspace subtitle did not identify the selected artifact.', {
detailSubtitle,
});
checks.push({ name: 'workspace-route-and-heading', ok: true, detailHeading, detailSubtitle, url: page.url() });
const detailErrorVisible = await page.getByText('Unable to load findings', { exact: true }).isVisible().catch(() => false);
await assert(!detailErrorVisible, 'Artifact workspace still rendered the findings error state.');
const findingCardCount = await page.locator('[data-finding-card]').count();
await assert(findingCardCount > 0, 'Artifact workspace did not render any finding cards.', { findingCardCount });
checks.push({ name: 'workspace-finding-cards', ok: true, findingCardCount });
await page.getByRole('tab', { name: 'Attestations' }).click({ timeout: 10_000 });
await settle(page);
await assert(page.url().includes('tab=attestations'), 'Workspace tab action did not update the route state.', {
url: page.url(),
});
checks.push({ name: 'workspace-tab-action', ok: true, url: page.url() });
await page.getByRole('link', { name: /Back to artifacts/i }).click({ timeout: 10_000 });
await waitForArtifactsSurface(page);
await settle(page);
await assert(page.url().includes('/triage/artifacts'), 'Workspace back navigation did not return to the artifacts route.', {
url: page.url(),
});
checks.push({ name: 'workspace-back-navigation', ok: true, url: page.url() });
const runtimeIssues = {
consoleErrors,
responseErrors,
requestFailures,
};
const summary = {
checkedAtUtc: new Date().toISOString(),
routePath,
checks,
runtimeIssues,
failedCheckCount: checks.filter((check) => !check.ok).length,
runtimeIssueCount:
runtimeIssues.consoleErrors.length
+ runtimeIssues.responseErrors.length
+ runtimeIssues.requestFailures.length,
};
await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
if (summary.failedCheckCount > 0 || summary.runtimeIssueCount > 0) {
process.exitCode = 1;
}
} catch (error) {
const summary = {
checkedAtUtc: new Date().toISOString(),
routePath,
checks,
error: error instanceof Error ? error.message : String(error),
consoleErrors,
responseErrors,
requestFailures,
};
await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
process.exitCode = 1;
} finally {
await context.close().catch(() => {});
await browser.close().catch(() => {});
}
}
await run();