#!/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();