#!/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-watchlist-action-sweep.json'); const authStatePath = path.join(outputDir, 'live-watchlist-action-sweep.state.json'); const authReportPath = path.join(outputDir, 'live-watchlist-action-sweep.auth.json'); const scopeQuery = 'tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d'; function uniqueSuffix() { return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } async function settle(page, timeout = 1_500) { await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {}); await page.waitForTimeout(timeout); } 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), message: (await page.locator('.message-banner').textContent().catch(() => '')).trim(), }; } 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 findNav(page, label) { const candidates = [ page.getByRole('tab', { name: label }).first(), page.getByRole('button', { name: label }).first(), page.getByRole('link', { name: label }).first(), ]; for (const candidate of candidates) { if ((await candidate.count()) > 0) { return candidate; } } return null; } async function openEntriesTab(page) { const locator = await findNav(page, 'Entries'); if (locator) { await locator.click({ timeout: 10_000 }); await settle(page); } } async function waitForSubmitEnabled(page, label) { await page.waitForFunction( (buttonLabel) => { const button = Array.from(document.querySelectorAll('button')) .find((candidate) => (candidate.textContent || '').replace(/\s+/g, ' ').trim() === buttonLabel); return !!button && !button.disabled; }, label, { timeout: 20_000 }, ); } async function rowLocator(page, text) { return page.locator('tr[data-testid="entry-row"]').filter({ hasText: text }).first(); } async function waitForRow(page, text) { const row = await rowLocator(page, text); await row.waitFor({ state: 'visible', timeout: 20_000 }); return row; } async function fillEntryForm(page, values) { const form = page.locator('form[data-testid="entry-form"]'); if (values.displayName !== undefined) { await form.locator('input[formcontrolname="displayName"]').fill(values.displayName); } if (values.issuer !== undefined) { await form.locator('input[formcontrolname="issuer"]').fill(values.issuer); } if (values.keyId !== undefined) { await form.locator('input[formcontrolname="keyId"]').fill(values.keyId); } } async function clickEntryAction(page, rowText, actionLabel) { const row = await waitForRow(page, rowText); await row.getByRole('button', { name: actionLabel }).click({ timeout: 10_000 }); await settle(page); } async function waitForMessage(page, text) { await page.waitForFunction( (expected) => { const banner = document.querySelector('.message-banner'); return !!banner && (banner.textContent || '').includes(expected); }, text, { timeout: 20_000 }, ); } async function main() { await mkdir(outputDir, { recursive: true }); let currentAction = 'authenticate-frontdoor'; let browser; let context; const runtime = { consoleErrors: [], pageErrors: [], responseErrors: [], requestFailures: [], }; const suffix = uniqueSuffix(); const createdName = `QA Watchlist ${suffix}`; const updatedName = `${createdName} Updated`; const duplicateName = `${createdName} Duplicate`; const issuer = `https://${suffix}.example.test`; const keyId = `kid-${suffix}`; const results = []; let page; try { const authReport = await authenticateFrontdoor({ statePath: authStatePath, reportPath: authReportPath, headless: true, }); browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'], }); context = await createAuthenticatedContext(browser, authReport, { statePath: authStatePath }); page = await context.newPage(); 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, }); } }); page.on('dialog', (dialog) => dialog.accept().catch(() => undefined)); currentAction = 'route:/setup/trust-signing/watchlist/entries'; await navigate(page, '/setup/trust-signing/watchlist/entries'); results.push({ action: 'route:/setup/trust-signing/watchlist/entries', ok: (await headingText(page)).length > 0 && (await page.getByTestId('watchlist-page').count()) > 0, snapshot: await captureSnapshot(page, 'watchlist-entries'), }); currentAction = 'create-entry'; await page.getByTestId('create-entry-btn').click({ timeout: 10_000 }); await settle(page); await fillEntryForm(page, { displayName: createdName, issuer, keyId }); await waitForSubmitEnabled(page, 'Create rule'); await page.getByRole('button', { name: 'Create rule' }).click({ timeout: 10_000 }); await waitForMessage(page, 'Watchlist entry created.'); await waitForRow(page, createdName); results.push({ action: 'create-entry', ok: true, snapshot: await captureSnapshot(page, 'after-create'), }); currentAction = 'edit-entry'; await clickEntryAction(page, createdName, 'Edit'); await fillEntryForm(page, { displayName: updatedName, issuer, keyId }); await waitForSubmitEnabled(page, 'Save changes'); await page.getByRole('button', { name: 'Save changes' }).click({ timeout: 10_000 }); await waitForMessage(page, 'Watchlist entry updated.'); await waitForRow(page, updatedName); results.push({ action: 'edit-entry', ok: true, snapshot: await captureSnapshot(page, 'after-edit'), }); currentAction = 'test-pattern'; await clickEntryAction(page, updatedName, 'Edit'); const testSection = page.locator('.test-section'); await testSection.locator('input[formcontrolname="issuer"]').fill(issuer); await testSection.locator('input[formcontrolname="keyId"]').fill(keyId); await page.getByRole('button', { name: 'Run test' }).click({ timeout: 10_000 }); await page.getByTestId('test-result').waitFor({ state: 'visible', timeout: 20_000 }); const testText = (await page.getByTestId('test-result').textContent().catch(() => '')).trim(); results.push({ action: 'test-pattern', ok: testText.includes('Match'), snapshot: await captureSnapshot(page, 'after-test-pattern'), detail: testText, }); await page.getByRole('button', { name: 'Close' }).click({ timeout: 10_000 }); await settle(page); currentAction = 'duplicate-entry'; await clickEntryAction(page, updatedName, 'Duplicate'); await fillEntryForm(page, { displayName: duplicateName, issuer, keyId }); await waitForSubmitEnabled(page, 'Create duplicate'); await page.getByRole('button', { name: 'Create duplicate' }).click({ timeout: 10_000 }); await waitForMessage(page, 'Watchlist entry duplicated.'); await waitForRow(page, duplicateName); results.push({ action: 'duplicate-entry', ok: true, snapshot: await captureSnapshot(page, 'after-duplicate'), }); currentAction = 'save-tuning'; await clickEntryAction(page, updatedName, 'Tune'); const tuningForm = page.locator('form[data-testid="tuning-form"]'); await tuningForm.locator('input[formcontrolname="suppressDuplicatesMinutes"]').fill('15'); await tuningForm.locator('textarea[formcontrolname="channelOverridesText"]').fill('slack:security'); await waitForSubmitEnabled(page, 'Save tuning'); await page.getByRole('button', { name: 'Save tuning' }).click({ timeout: 10_000 }); await page.waitForFunction( () => { const banner = document.querySelector('.message-banner'); return !!banner && (banner.textContent || '').includes('Saved tuning for'); }, null, { timeout: 20_000 }, ); results.push({ action: 'save-tuning', ok: true, snapshot: await captureSnapshot(page, 'after-save-tuning'), }); currentAction = 'alerts-tab'; const alertsTab = await findNav(page, 'Alerts'); if (alertsTab) { await alertsTab.click({ timeout: 10_000 }); await settle(page); const alertRows = await page.locator('tr[data-testid="alert-row"]').count(); const emptyState = (await page.locator('.empty-state').first().textContent().catch(() => '')).trim(); results.push({ action: 'alerts-tab', ok: alertRows > 0 || emptyState.includes('No alerts match the current scope'), snapshot: await captureSnapshot(page, 'alerts-tab'), detail: alertRows > 0 ? `${alertRows} alert rows` : emptyState, }); } currentAction = 'delete-duplicate'; await openEntriesTab(page); await clickEntryAction(page, duplicateName, 'Delete'); await waitForMessage(page, 'Watchlist entry deleted.'); await page.waitForFunction( (name) => !Array.from(document.querySelectorAll('tr[data-testid="entry-row"]')) .some((row) => (row.textContent || '').includes(name)), duplicateName, { timeout: 20_000 }, ); results.push({ action: 'delete-duplicate', ok: true, snapshot: await captureSnapshot(page, 'after-delete-duplicate'), }); currentAction = 'delete-original'; await clickEntryAction(page, updatedName, 'Delete'); await waitForMessage(page, 'Watchlist entry deleted.'); await page.waitForFunction( (name) => !Array.from(document.querySelectorAll('tr[data-testid="entry-row"]')) .some((row) => (row.textContent || '').includes(name)), updatedName, { timeout: 20_000 }, ); results.push({ action: 'delete-original', ok: true, snapshot: await captureSnapshot(page, 'after-delete-original'), }); 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`); } catch (error) { const summary = { generatedAtUtc: new Date().toISOString(), failedAction: currentAction, error: error instanceof Error ? error.message : String(error), results, runtime, finalUrl: page?.url() ?? null, }; await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); process.stderr.write(`[live-watchlist-action-sweep] ${summary.error}\n`); process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); process.exitCode = 1; } finally { await context?.close().catch(() => undefined); await browser?.close().catch(() => undefined); } } main();