import { expect, test, type Page } from "@playwright/test"; async function setEditor(page: Page, testId: string, value: string) { const content = page.getByTestId(testId).locator(".cm-content"); await content.fill(value); } async function waitForReady(page: Page) { await expect(page.locator(".run-status")).toHaveClass(/status-ready/u, { timeout: 15_000, }); } test("standalone ECMAScript workbench parses, explains and extracts", async ({ page, }) => { const pageErrors: string[] = []; page.on("pageerror", (error) => pageErrors.push(error.message)); await page.goto("./"); await expect( page.getByRole("heading", { name: "Regex Tools" }), ).toBeVisible(); await waitForReady(page); await expect( page.getByRole("heading", { name: "Explanation tree" }), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Extraction tree" }), ).toBeVisible(); await expect(page.locator(".extraction-row")).toHaveCount(8); await expect(page.getByText("alice", { exact: true }).first()).toBeVisible(); await expect( page.getByText("Bérénice", { exact: true }).first(), ).toBeVisible(); await expect( page.getByRole("columnheader", { name: "Native start" }), ).toBeVisible(); await expect( page.getByRole("checkbox", { name: "Global (g)" }), ).toBeChecked(); await expect(page.locator(".cm-capture-1").first()).toHaveAttribute( "title", /group 1 · date/u, ); await expect( page .locator('[aria-live="polite"]') .filter({ hasText: "Completed locally" }), ).toHaveCount(1); expect(pageErrors).toEqual([]); }); test("editing reports malformed syntax and recovers with exact matches", async ({ page, }) => { await page.goto("./"); await waitForReady(page); const patternEditor = page .getByTestId("editor-regular-expression-pattern") .locator(".cm-content"); const originalPattern = await patternEditor.textContent(); await patternEditor.click(); await patternEditor.press("End"); await patternEditor.press("x"); await expect(patternEditor).toHaveText(`${originalPattern}x`); await setEditor(page, "editor-regular-expression-pattern", "("); await expect( page .locator(".diagnostic-error") .filter({ hasText: /unterminated|invalid/iu }), ).toBeVisible(); await expect(page.locator(".run-status")).toContainText( "Execution is paused", ); await setEditor(page, "editor-regular-expression-pattern", "(?\\w+)"); await setEditor(page, "editor-test-text", "hello world"); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.locator(".extraction-row").first()).toContainText("hello"); }); test("rapid pattern and replacement edits cannot run with stale syntax", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await page.getByRole("button", { name: "Replace", exact: true }).click(); await setEditor(page, "editor-regular-expression-pattern", "(?\\w+)"); await setEditor(page, "editor-replacement-template", "$"); await setEditor( page, "editor-regular-expression-pattern", "(?[A-Z])", ); await setEditor(page, "editor-replacement-template", "[$]"); await setEditor(page, "editor-test-text", "A B"); const runButton = page.getByRole("button", { name: "Run", exact: true }); await expect(runButton).toBeDisabled(); await runButton.evaluate((button: HTMLButtonElement) => button.click()); await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u); await expect(page.locator(".extraction-row")).toHaveCount(0); await expect(runButton).toBeEnabled(); const currentNamedToken = page.locator(".token-row").filter({ has: page.locator("code", { hasText: "$" }), }); await expect(currentNamedToken.locator("code")).toHaveText("$"); await expect(currentNamedToken.locator("small")).toHaveText("named capture"); await page.getByRole("checkbox", { name: "Ignore case (i)" }).check(); await expect(runButton).toBeDisabled(); await runButton.evaluate((button: HTMLButtonElement) => button.click()); await expect(page.locator(".run-status")).not.toHaveClass(/status-running/u); await expect(runButton).toBeEnabled(); await runButton.click(); await waitForReady(page); await expect( page.getByTestId("editor-replacement-output").locator(".cm-content"), ).toHaveText("[A] [B]"); }); test("manual editing clears results that no longer describe the inputs", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor(page, "editor-regular-expression-pattern", "nomatch"); await expect(page.locator(".run-status")).toHaveClass(/status-idle/u); await expect(page.locator(".run-status")).toContainText( "previous results were cleared", ); await expect(page.locator(".run-status small")).toHaveCount(0); await expect(page.locator(".extraction-row")).toHaveCount(0); await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0); await expect(page.locator(".cm-subject-match")).toHaveCount(0); }); test("capture-group resource limits block execution visibly", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor( page, "editor-regular-expression-pattern", "()".repeat(1_001), ); await expect(page.locator(".run-status")).toHaveClass(/status-error/u); await expect(page.locator(".run-status")).toContainText( "above the 1,000 group execution limit", ); await expect(page.locator(".run-status small")).toHaveCount(0); await expect(page.locator(".extraction-row")).toHaveCount(0); }); test("large valid patterns bound syntax tree nodes and editor decorations", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor(page, "editor-regular-expression-pattern", "a".repeat(2_100)); await expect(page.getByTestId("pattern-mark-render-limit")).toBeVisible(); await expect(page.getByTestId("explanation-render-limit")).toContainText( "2,000", ); await expect( page .getByRole("tree", { name: "Pattern explanation" }) .getByRole("treeitem"), ).toHaveCount(2_000); }); test("the pattern editor rejects input above its hard resource limit", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); const editor = page .getByTestId("editor-regular-expression-pattern") .locator(".cm-content"); const previousPattern = await editor.textContent(); await editor.fill("a".repeat(64 * 1_024 + 1)); await expect(page.getByRole("alert")).toContainText( "limited to 65,536 UTF-16 units", ); await expect(editor).toHaveText(previousPattern ?? ""); }); test("large match sets bound tree nodes and editor highlights explicitly", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor(page, "editor-regular-expression-pattern", "a"); await setEditor(page, "editor-test-text", "a".repeat(10_001)); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.locator(".run-status")).toContainText("10,000 matches"); await expect(page.locator(".extraction-row")).toHaveCount(2_000); await expect(page.getByTestId("extraction-render-limit")).toContainText( "first 2,000 of 20,000", ); await expect(page.getByTestId("subject-mark-render-limit")).toContainText( "first 2,000 of 10,000", ); await expect( page.locator(".diagnostic").filter({ hasText: "configured limit" }), ).toBeVisible(); }); test("list export stays disabled when the engine returned a limited prefix", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await page.getByRole("button", { name: "List", exact: true }).click(); await setEditor(page, "editor-regular-expression-pattern", "a"); await setEditor(page, "editor-test-text", "a".repeat(10_001)); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.getByText(/engine limit reached/u)).toBeVisible(); await expect( page.getByText(/engine stopped collecting matches/u), ).toBeVisible(); await expect( page.getByRole("button", { name: "Download TEXT" }), ).toBeDisabled(); }); test("subject caret selection identifies a capture without replacing it", async ({ page, }) => { await page.goto("./"); await waitForReady(page); const subjectEditor = page .getByTestId("editor-test-text") .locator(".cm-content"); const originalSubject = await subjectEditor.textContent(); await subjectEditor.click(); await subjectEditor.press("Control+Home"); await expect( page .getByRole("tree", { name: "Match extraction" }) .locator(".tree-row.is-selected"), ).toContainText("Group 1 — date"); await expect( page .getByRole("grid", { name: "Captured matches" }) .locator("tbody tr") .first(), ).toHaveAttribute("aria-selected", "true"); expect( await subjectEditor.evaluate((editor) => { const selection = globalThis.getSelection(); return ( selection?.isCollapsed === true && editor.contains(selection.anchorNode) ); }), ).toBe(true); await subjectEditor.press("x"); await expect(subjectEditor).toHaveText(`x${originalSubject}`); }); test("test-text whitespace and caret position are visible on request", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor(page, "editor-test-text", "a b\ncd"); await expect(page.getByText("Ln 2, Col 3", { exact: true })).toBeVisible(); await page.getByRole("checkbox", { name: "Visible whitespace" }).check(); await expect( page.getByTestId("editor-test-text").locator(".cm-highlightSpace"), ).toHaveCount(1); }); test("capture-row selection remains synchronized after editor highlighting", async ({ page, }) => { await page.goto("./"); await waitForReady(page); const captureRows = page .getByRole("grid", { name: "Captured matches" }) .locator("tbody tr"); await captureRows.first().focus(); await captureRows.first().press("Enter"); await expect(captureRows.first()).toHaveAttribute("aria-selected", "true"); await expect(captureRows.first()).toHaveClass(/is-selected/u); await expect( page .getByRole("tree", { name: "Match extraction" }) .locator(".tree-row.is-selected"), ).toContainText("Group 1 — date"); await expect( page.getByTestId("editor-test-text").locator(".cm-selected-range"), ).toHaveCount(1); await page .getByRole("tree", { name: "Match extraction" }) .getByRole("treeitem", { name: /^Match 1 —/u }) .click(); await expect( page .getByRole("tree", { name: "Pattern explanation" }) .locator(".tree-row.is-selected"), ).toContainText("Regular-expression pattern"); }); test("pattern explanation selection synchronizes capture result views", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page .getByRole("tree", { name: "Pattern explanation" }) .locator(".tree-row") .filter({ hasText: "group 1 · date" }) .first() .click(); await expect( page .getByRole("tree", { name: "Match extraction" }) .locator(".tree-row.is-selected"), ).toContainText("Group 1 — date"); await expect( page .getByRole("grid", { name: "Captured matches" }) .locator("tbody tr") .first(), ).toHaveAttribute("aria-selected", "true"); await expect( page.getByTestId("editor-test-text").locator(".cm-selected-range"), ).toHaveCount(1); }); test("replacement and list modes use native and typed templates", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByRole("button", { name: "Replace", exact: true }).click(); await expect( page.getByRole("heading", { name: "Replacement tokens" }), ).toBeVisible(); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect( page.getByTestId("editor-replacement-output").locator(".cm-content"), ).toContainText("alice — 2026-07-24"); const perMatchPreview = page.locator(".replacement-preview-panel"); await expect( perMatchPreview.getByRole("heading", { name: "Per-match preview" }), ).toBeVisible(); await expect( perMatchPreview.locator(".replacement-match-preview"), ).toHaveCount(2); await expect( perMatchPreview.locator(".replacement-match-preview").first(), ).toContainText("2026-07-24 alice"); await expect( perMatchPreview.locator(".replacement-match-preview").first(), ).toContainText("alice — 2026-07-24"); const userContribution = perMatchPreview .getByRole("button", { name: /Insert named capture “user”/u }) .first(); await userContribution.click(); await expect(userContribution).toHaveAttribute("aria-pressed", "true"); await expect( page .getByRole("tree", { name: "Match extraction" }) .locator(".tree-row.is-selected"), ).toContainText("Group 2 — user"); await expect( page .getByTestId("editor-replacement-template") .locator(".cm-selected-range"), ).toHaveCount(1); await expect( page .getByTestId("editor-regular-expression-pattern") .locator(".cm-selected-range"), ).toHaveCount(1); await expect( page.getByTestId("editor-test-text").locator(".cm-selected-range"), ).toHaveCount(1); await expect( page.getByTestId("editor-replacement-output").locator(".cm-selected-range"), ).toHaveCount(1); await page.getByRole("button", { name: "List", exact: true }).click(); await expect( page.getByRole("heading", { name: "Generated rows" }), ).toBeVisible(); await expect(page.locator(".list-preview li")).toHaveCount(2); await expect(page.locator(".list-preview li").first()).toContainText( "alice — 2026-07-24", ); }); test("sticky scan-all replacement uses every authoritative match", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByRole("checkbox", { name: "Global (g)" }).press("Space"); await page.getByRole("checkbox", { name: "Unicode (u)" }).press("Space"); await page.getByRole("checkbox", { name: "Sticky (y)" }).press("Space"); await page.getByRole("checkbox", { name: "Scan all" }).check(); await setEditor(page, "editor-regular-expression-pattern", "a"); await setEditor(page, "editor-test-text", "aa"); await waitForReady(page); await page.getByRole("button", { name: "Replace", exact: true }).click(); await setEditor(page, "editor-replacement-template", "x"); await waitForReady(page); await expect(page.locator(".run-status")).toContainText("2 matches"); await expect( page .getByRole("tree", { name: "Match extraction" }) .getByRole("treeitem", { name: /^Match \d+ —/u }), ).toHaveCount(2); await expect( page.getByTestId("editor-replacement-output").locator(".cm-content"), ).toHaveText("xx"); }); test("unit tests run through the actual worker engine", async ({ page }) => { await page.goto("./"); await waitForReady(page); await page.getByRole("button", { name: "Unit tests", exact: true }).click(); await page .getByRole("textbox", { name: "Name", exact: true }) .fill("Current dates"); await page.getByRole("button", { name: "Add test" }).click(); await page.getByRole("button", { name: "Run all" }).click(); await expect(page.locator(".test-result")).toContainText("Pass", { timeout: 15_000, }); }); test("unit-test cases can be captured, cloned, edited, selectively run, filtered, and moved as JSON", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByRole("button", { name: "Unit tests", exact: true }).click(); await page.getByRole("button", { name: "Add current result" }).click(); await page .getByRole("button", { name: "Clone Current match result" }) .click(); await page .getByRole("button", { name: "Edit Current match result copy" }) .click(); await page .getByRole("textbox", { name: "Name", exact: true }) .fill("Expected failure"); await page .getByRole("combobox", { name: "Expectation" }) .selectOption("should-not-match"); await page.getByRole("button", { name: "Update test" }).click(); await page .getByRole("checkbox", { name: "Select Current match result" }) .check(); await page.getByRole("button", { name: "Run selected" }).click(); await expect( page .locator(".test-list li") .filter({ hasText: "Current match result" }) .first() .locator(".test-result"), ).toContainText("Pass", { timeout: 15_000 }); await expect( page .locator(".test-list li") .filter({ hasText: "Expected failure" }) .locator(".test-result"), ).toHaveText("Not run"); await page.getByRole("checkbox", { name: "Select Expected failure" }).check(); await page.getByRole("button", { name: "Run selected" }).click(); await expect( page .locator(".test-list li") .filter({ hasText: "Expected failure" }) .locator(".test-result"), ).toContainText("Fail", { timeout: 15_000 }); await page.getByRole("checkbox", { name: "Failures only" }).check(); await expect(page.locator(".test-list li")).toHaveCount(1); await expect(page.locator(".test-list li")).toContainText("Expected failure"); await page .getByRole("checkbox", { name: "Include subjects in JSON" }) .check(); const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: "Export JSON" }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe("regex-tests.regex-tests.json"); const downloadedPath = await download.path(); if (!downloadedPath) throw new Error("Test-suite download path unavailable"); await page .getByLabel("Import test-suite JSON file") .setInputFiles(downloadedPath); await expect(page.getByText("Imported 2 tests.")).toBeVisible(); await page.getByRole("checkbox", { name: "Failures only" }).uncheck(); await expect(page.locator(".test-list li")).toHaveCount(4); }); test("a running unit-test suite can be cancelled without affecting interactive execution", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$"); await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`); await page.getByRole("button", { name: "Unit tests", exact: true }).click(); await page .getByRole("combobox", { name: "Expectation" }) .selectOption("must-time-out"); await page .getByRole("spinbutton", { name: "Per-test timeout (ms, optional)" }) .fill("10000"); await page.getByRole("button", { name: "Add test" }).click(); await page.getByRole("button", { name: "Run all" }).click(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page.getByRole("button", { name: "Run all" })).toBeEnabled(); await page.getByRole("button", { name: "Match", exact: true }).click(); await setEditor(page, "editor-regular-expression-pattern", "^ok$"); await setEditor(page, "editor-test-text", "ok"); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.locator(".run-status")).toContainText("1 matches"); }); test("invalid Toolbox context degrades to the complete standalone app", async ({ page, }) => { await page.goto( "./?toolbox=https%3A%2F%2Fcross-origin.invalid%2Ftoolbox.catalog.json", ); await expect( page.getByRole("heading", { name: "Regex Tools" }), ).toBeVisible(); await waitForReady(page); await expect( page.getByRole("button", { name: "Run", exact: true }), ).toBeEnabled(); }); test("a valid same-origin Toolbox context connects the shared app switcher", async ({ page, }) => { await page.goto("./?toolbox=/toolbox.catalog.json"); await waitForReady(page); await expect(page.locator(".toolbox-shell")).toHaveAttribute( "data-toolbox-context", "connected", ); await page.getByRole("button", { name: "Apps" }).click(); const switcher = page.getByRole("navigation", { name: "Toolbox applications", }); await expect(switcher).toBeVisible(); await expect( switcher.getByRole("link", { name: "Regex Tools" }), ).toHaveAttribute("aria-current", "page"); }); test("the same production build loads from a deep nested path", async ({ page, }) => { const responses: string[] = []; page.on("response", (response) => { if (response.status() >= 400) responses.push(response.url()); }); await page.goto("/deep/nested/regex/"); await expect( page.getByRole("heading", { name: "Regex Tools" }), ).toBeVisible(); await waitForReady(page); await expect(page.locator(".extraction-row")).toHaveCount(8); expect(responses).toEqual([]); }); test("responsive layout retains editors and result trees", async ({ page }) => { await page.setViewportSize({ width: 320, height: 844 }); await page.goto("./"); await waitForReady(page); await expect( page.getByTestId("editor-regular-expression-pattern"), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Explanation tree" }), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Extraction tree" }), ).toBeVisible(); const overflows = await page.evaluate( () => document.documentElement.scrollWidth > window.innerWidth, ); expect(overflows).toBe(false); }); test("dark theme capture and success text meets normal-text contrast", async ({ page, }) => { await page.emulateMedia({ colorScheme: "dark" }); await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await setEditor( page, "editor-regular-expression-pattern", "(a)(b)(c)(d)(e)(f)(g)(h)", ); await setEditor(page, "editor-test-text", "abcdefgh"); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.locator(".cm-capture-8").first()).toBeAttached(); const ratios = await page.evaluate(() => { const channels = (value: string): [number, number, number] => { const values = value.match(/[\d.]+/gu)?.map(Number) ?? []; if (value.startsWith("color(srgb")) { return [values[0]! * 255, values[1]! * 255, values[2]! * 255]; } return [values[0]!, values[1]!, values[2]!]; }; const luminance = (color: [number, number, number]) => { const normalized = color.map((channel) => { const value = channel / 255; return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; }); return ( 0.2126 * normalized[0]! + 0.7152 * normalized[1]! + 0.0722 * normalized[2]! ); }; const contrast = (foreground: string, background: string) => { const left = luminance(channels(foreground)); const right = luminance(channels(background)); return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05); }; const editor = document.querySelector(".code-editor"); if (!editor) throw new Error("Editor is unavailable."); const editorBackground = getComputedStyle(editor).backgroundColor; const captureRatios = Array.from({ length: 8 }, (_, index) => { const capture = document.querySelector( `.cm-capture-${index + 1}`, ); if (!capture) throw new Error(`Capture ${index + 1} is unavailable.`); return contrast(getComputedStyle(capture).color, editorBackground); }); const success = document.querySelector( ".run-status.status-ready > span", ); if (!success) throw new Error("Success status is unavailable."); return [ ...captureRatios, contrast( getComputedStyle(success).color, getComputedStyle(success).backgroundColor, ), ]; }); expect(Math.min(...ratios)).toBeGreaterThanOrEqual(4.5); }); test("project file picker is named and omitted from keyboard order", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByRole("button", { name: "Project", exact: true }).click(); const input = page.getByLabel("Import regex project JSON file"); await expect(input).toHaveAttribute("tabindex", "-1"); await expect(input).toHaveAttribute("type", "file"); }); test("result trees and capture rows support keyboard navigation", async ({ page, }) => { await page.goto("./"); await waitForReady(page); const explanationItems = page .getByRole("tree", { name: "Pattern explanation" }) .getByRole("treeitem"); await explanationItems.first().focus(); await explanationItems.first().press("ArrowDown"); await expect(explanationItems.nth(1)).toBeFocused(); const captureRows = page .getByRole("grid", { name: "Captured matches" }) .locator("tbody tr"); await captureRows.first().focus(); await captureRows.first().press("ArrowDown"); await expect(captureRows.nth(1)).toBeFocused(); const globalFlag = page.getByRole("checkbox", { name: "Global (g)" }); await globalFlag.focus(); const flagFocus = await globalFlag.evaluate((input) => { const indicator = input.nextElementSibling; if (!(indicator instanceof HTMLElement)) return null; const style = getComputedStyle(indicator); return { style: style.outlineStyle, width: style.outlineWidth }; }); expect(flagFocus).toEqual({ style: "solid", width: "3px" }); }); test("a timeout kills the worker and the next request uses a fresh engine", async ({ page, }) => { await page.goto("./"); await waitForReady(page); await page.getByLabel("Live").uncheck(); await page.getByLabel("Manual limit").selectOption("250"); await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$"); await setEditor(page, "editor-test-text", `${"a".repeat(48)}!`); await page.getByRole("button", { name: "Run", exact: true }).click(); await expect(page.locator(".run-status")).toHaveClass(/status-timeout/u, { timeout: 15_000, }); await expect(page.locator(".run-status")).toContainText( "worker was terminated", ); await expect(page.locator(".run-status small")).toHaveCount(0); await expect(page.locator(".extraction-row")).toHaveCount(0); await expect(page.locator(".capture-table-scroll tbody tr")).toHaveCount(0); await expect(page.locator(".cm-subject-match")).toHaveCount(0); await setEditor(page, "editor-regular-expression-pattern", "^ok$"); await setEditor(page, "editor-test-text", "ok"); await page.getByRole("button", { name: "Run", exact: true }).click(); await waitForReady(page); await expect(page.locator(".run-status")).toContainText("1 matches"); });