#!/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 __dirname = path.dirname(fileURLToPath(import.meta.url)); const webRoot = path.resolve(__dirname, '..'); const outputDir = path.join(webRoot, 'output', 'playwright'); const outputPath = path.join(outputDir, 'live-notifications-watchlist-recheck.json'); const authStatePath = path.join(outputDir, 'live-notifications-watchlist-recheck.state.json'); const authReportPath = path.join(outputDir, 'live-notifications-watchlist-recheck.auth.json'); const scopeQuery = 'tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d'; async function settle(page) { await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {}); await page.waitForTimeout(1_500); } 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) { const alerts = await page .locator('[role="alert"], .mat-mdc-snack-bar-container, .toast, .notification, .error-banner') .evaluateAll((nodes) => nodes .map((node) => (node.textContent || '').trim().replace(/\s+/g, ' ')) .filter(Boolean) .slice(0, 5), ) .catch(() => []); return { label, url: page.url(), title: await page.title(), heading: await headingText(page), alerts, }; } async function navigate(page, route) { const separator = route.includes('?') ? '&' : '?'; const url = `https://stella-ops.local${route}${separator}${scopeQuery}`; await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); await settle(page); return url; } async function clickLinkAndVerify(page, route, linkName, expectedPath) { await navigate(page, route); const locator = page.getByRole('link', { name: linkName }).first(); if ((await locator.count()) === 0) { return { action: `link:${linkName}`, ok: false, reason: 'missing-link', snapshot: await captureSnapshot(page, `missing-link:${linkName}`), }; } await locator.click({ timeout: 10_000 }); await page.waitForURL((url) => url.pathname.includes(expectedPath), { timeout: 15_000 }); await settle(page); return { action: `link:${linkName}`, ok: page.url().includes(expectedPath), snapshot: await captureSnapshot(page, `after-link:${linkName}`), }; } async function main() { 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 runtime = { consoleErrors: [], pageErrors: [], responseErrors: [], requestFailures: [], }; page.on('console', (message) => { if (message.type() === 'error') { runtime.consoleErrors.push({ page: page.url(), text: message.text() }); } }); page.on('pageerror', (error) => { runtime.pageErrors.push({ page: page.url(), message: error.message }); }); page.on('requestfailed', (request) => { const url = request.url(); if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return; } runtime.requestFailures.push({ page: page.url(), method: request.method(), url, error: request.failure()?.errorText ?? 'unknown', }); }); page.on('response', (response) => { const url = response.url(); if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return; } if (response.status() >= 400) { runtime.responseErrors.push({ page: page.url(), method: response.request().method(), status: response.status(), url, }); } }); const results = []; results.push(await clickLinkAndVerify( page, '/ops/operations/notifications', 'Open watchlist tuning', '/setup/trust-signing/watchlist/tuning', )); results.push(await clickLinkAndVerify( page, '/ops/operations/notifications', 'Review watchlist alerts', '/setup/trust-signing/watchlist/alerts', )); const summary = { generatedAtUtc: new Date().toISOString(), results, runtime, }; await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); await browser.close(); } main().catch((error) => { process.stderr.write(`[live-notifications-watchlist-recheck] ${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; });