import type { Page } from '@playwright/test'; export interface ClickEverythingOptions { /** * Substrings matched against each element's outerHTML to skip destructive * interactions. The helper extracts the opening tag (first 200 chars of * outerHTML) so test-ids, aria labels, class names, and tag names are all * matchable. Default skips cover archive/delete/restore/transfer/sign-out. */ skip?: string[]; /** * Optional cleanup hook invoked after each click (and after auto-closing any * dialog opened by the click). Useful when a click flips global state and * the next iteration needs the page fresh. */ cleanupBetween?: () => Promise; /** * Per-click timeout in ms. Defaults to 2000. */ clickTimeoutMs?: number; /** * If true (default), the helper navigates back to the starting URL after * each click that changed the URL. Set to false for pages where the goal is * to follow links (e.g. nav menus) end-to-end. */ returnToStart?: boolean; } export interface ClickEverythingResult { clicked: number; skipped: number; errors: string[]; } const DEFAULT_SKIP = [ 'data-testid="archive"', 'data-testid="delete"', 'data-testid="restore"', 'data-testid="transfer-yacht"', 'data-testid="sign-out"', 'aria-label="Sign out"', 'aria-label="Log out"', '>Archive<', '>Delete<', '>Restore<', '>Transfer<', ]; /** * Click every visible button, link, and role=button on the current page and * record any console errors, network 4xx/5xx responses, or click failures. * * Why: a fast smoke check that no UI element 500s, throws, or routes to a * stale endpoint after a refactor — without writing per-button assertions. * * The helper is intentionally tolerant: a single click that fails only adds * to `errors` rather than throwing, so the caller can attribute failures to * specific elements via the returned list. */ export async function clickEverythingOnPage( page: Page, opts: ClickEverythingOptions = {}, ): Promise { const startingUrl = page.url(); const errors: string[] = []; let clicked = 0; let skipped = 0; const skipPatterns = [...DEFAULT_SKIP, ...(opts.skip ?? [])]; const clickTimeout = opts.clickTimeoutMs ?? 2000; const returnToStart = opts.returnToStart ?? true; const onConsole = (msg: import('@playwright/test').ConsoleMessage) => { if (msg.type() === 'error') { errors.push(`[console] ${msg.text().slice(0, 300)}`); } }; const onResponse = (resp: import('@playwright/test').Response) => { if (resp.status() >= 400) { const url = resp.url(); // Ignore third-party noise; focus on app endpoints. if (url.includes(new URL(startingUrl).host)) { errors.push(`[network] ${resp.status()} ${url}`); } } }; page.on('console', onConsole); page.on('response', onResponse); try { const elements = await page.locator(':is(button, a, [role="button"])').all(); for (const el of elements) { let outerHtml = ''; try { outerHtml = (await el.evaluate((n) => (n as HTMLElement).outerHTML)).slice(0, 200); } catch { // Element detached between locator and evaluate — skip silently. continue; } if (skipPatterns.some((pat) => outerHtml.includes(pat))) { skipped++; continue; } try { if (!(await el.isVisible({ timeout: 250 }).catch(() => false))) { continue; } await el.click({ timeout: clickTimeout }); clicked++; // Let any async UI settle (navigation, fetches, animations). await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => undefined); // If a dialog opened, close it so the next click isn't blocked. const dialogClose = page.locator('[role="dialog"] [aria-label="Close"]').first(); if (await dialogClose.isVisible({ timeout: 250 }).catch(() => false)) { await dialogClose.click({ timeout: 1000 }).catch(() => undefined); } else { // Best-effort Esc to dismiss any open menu/popover. await page.keyboard.press('Escape').catch(() => undefined); } if (returnToStart && page.url() !== startingUrl) { await page.goto(startingUrl, { waitUntil: 'networkidle' }).catch(() => undefined); } if (opts.cleanupBetween) { await opts.cleanupBetween(); } } catch (err) { errors.push(`[click] ${outerHtml.slice(0, 100)} → ${(err as Error).message}`); } } } finally { page.off('console', onConsole); page.off('response', onResponse); } return { clicked, skipped, errors }; }