219 lines
8.2 KiB
JavaScript
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();
|