///
import { expect, test, type Page } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { unzipSync } from "fflate";
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("PCRE2 WebAssembly executes flavour-only syntax and native substitution", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("./");
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await page.getByRole("combobox", { name: "Flavour" }).selectOption("pcre2");
await expect(
page.getByRole("spinbutton", { name: "Match steps" }),
).toHaveValue("1000000");
await setEditor(
page,
"editor-regular-expression-pattern",
"(?|(a)|(b))-(?\\d+)",
);
await setEditor(page, "editor-test-text", "b-42");
const run = page.getByRole("button", { name: "Run", exact: true });
await expect(run).toBeEnabled();
await run.click();
await waitForReady(page);
await expect(page.locator(".run-status")).toContainText(
"native offsets utf8-byte",
);
await expect(page.locator(".extraction-row").first()).toContainText("b-42");
await expect(page.getByText("42", { exact: true }).first()).toBeVisible();
await page.getByRole("button", { name: "Replace", exact: true }).click();
await setEditor(page, "editor-replacement-template", "${number}:$1");
await expect(run).toBeEnabled();
await run.click();
await waitForReady(page);
await expect(
page.getByTestId("editor-replacement-output").locator(".cm-content"),
).toHaveText("42:b");
await page.getByRole("button", { name: "Quick reference" }).click();
await expect(
page.getByRole("dialog", { name: "Quick reference" }),
).toContainText("Branch-reset group");
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Capabilities" }).click();
const capabilities = page.getByRole("dialog", { name: "Capabilities" });
await expect(capabilities).toContainText("PCRE2 WebAssembly");
await expect(capabilities).toContainText("10.47 2025-10-21");
expect(pageErrors).toEqual([]);
});
test("PCRE2 trace viewer reports bounded automatic callouts in its own worker", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("./");
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await page.getByRole("combobox", { name: "Flavour" }).selectOption("pcre2");
await setEditor(page, "editor-regular-expression-pattern", "(a+)+$");
await setEditor(page, "editor-test-text", "aaaa!");
await page.getByRole("button", { name: "PCRE trace" }).click();
const dialog = page.getByRole("dialog", { name: "Execution trace" });
await expect(dialog).toBeVisible();
await expect(dialog).toContainText("reported automatic callouts");
await dialog.getByRole("spinbutton", { name: "Maximum events" }).fill("1");
await dialog
.getByRole("spinbutton", { name: "Maximum serialized bytes" })
.fill("4096");
await dialog.getByRole("button", { name: "Run trace" }).click();
await expect(dialog.locator(".trace-status")).toHaveClass(/status-ready/u, {
timeout: 15_000,
});
await expect(dialog.locator(".trace-table tbody tr")).toHaveCount(1);
await expect(dialog).toContainText("native status -37");
await expect(dialog).toContainText("one exact pcre2_match() invocation");
await expect(dialog).toContainText("configured event or serialized-byte cap");
await dialog.getByRole("button", { name: "Select trace event 1" }).click();
await expect(dialog).not.toBeVisible();
expect(pageErrors).toEqual([]);
});
test("ECMAScript analysis runs bounded cold and warm samples outside trace mode", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("./");
await waitForReady(page);
await page.getByRole("button", { name: "Analysis" }).click();
const dialog = page.getByRole("dialog", {
name: "Performance & risk analysis",
});
await expect(dialog).toBeVisible();
await expect(dialog).toContainText("Potential risk findings");
await dialog.getByRole("tab", { name: "Benchmark" }).click();
await expect(dialog).toContainText("Trace mode is never used");
await dialog.getByRole("spinbutton", { name: "Warm-up samples" }).fill("0");
await dialog.getByRole("spinbutton", { name: "Measured samples" }).fill("2");
await dialog.getByRole("button", { name: "Run bounded benchmark" }).click();
await expect(
dialog.getByRole("heading", { name: "Benchmark result" }),
).toBeVisible({ timeout: 15_000 });
await expect(dialog).toContainText("2 measured warm samples");
await expect(dialog).toContainText("Native ECMAScript RegExp");
expect(pageErrors).toEqual([]);
});
test("serves the bundled PCRE2 runtime with WebAssembly MIME", async ({
request,
}) => {
const moduleResponse = await request.get("./engines/pcre2/pcre2.mjs");
expect(moduleResponse.ok()).toBe(true);
expect(moduleResponse.headers()["content-type"]).toContain("text/javascript");
const response = await request.get("./engines/pcre2/pcre2.wasm");
expect(response.ok()).toBe(true);
expect(response.headers()["content-type"]).toContain("application/wasm");
expect((await response.body()).subarray(0, 4)).toEqual(
Buffer.from([0x00, 0x61, 0x73, 0x6d]),
);
});
test("quick reference and capabilities open as populated in-viewport dialogs", async ({
page,
}) => {
await page.goto("./");
await waitForReady(page);
await page
.getByRole("button", { name: "Quick reference", exact: true })
.click();
const referenceDialog = page.getByRole("dialog", {
name: "Quick reference",
});
await expect(referenceDialog).toBeVisible();
await expect(referenceDialog).toBeInViewport();
await expect(
referenceDialog.getByRole("button", { name: "Close quick reference" }),
).toBeFocused();
await expect(
referenceDialog.getByRole("listitem").filter({ hasText: "Literal" }),
).toBeVisible();
await referenceDialog
.getByLabel("Filter constructs")
.fill("not a reference entry");
await expect(referenceDialog.getByRole("status")).toContainText(
"No reference constructs match",
);
await page.keyboard.press("Escape");
await expect(referenceDialog).not.toBeVisible();
await expect(
page.getByRole("button", { name: "Quick reference", exact: true }),
).toHaveAttribute("aria-expanded", "false");
await page.getByRole("button", { name: "Capabilities", exact: true }).click();
const capabilitiesDialog = page.getByRole("dialog", {
name: "Capabilities",
});
await expect(capabilitiesDialog).toBeVisible();
await expect(capabilitiesDialog).toBeInViewport();
await expect(
capabilitiesDialog.getByRole("button", { name: "Close capabilities" }),
).toBeFocused();
await expect(capabilitiesDialog).toContainText("regexpp-community 4.12.2");
await expect(capabilitiesDialog).toContainText("Native ECMAScript RegExp");
await capabilitiesDialog
.getByRole("button", { name: "Close capabilities" })
.click();
await expect(capabilitiesDialog).not.toBeVisible();
});
test("corpus mode scans local text, applies exact replacements, and exports safely", async ({
page,
}) => {
await page.goto("./");
await waitForReady(page);
await page
.getByRole("button", { name: "Corpus / Apply", exact: true })
.click();
await expect(
page.getByRole("heading", { name: "Corpus documents" }),
).toBeVisible();
await expect(page.getByTestId("editor-test-text")).not.toBeVisible();
await page
.getByRole("textbox", { name: "Pasted corpus text" })
.fill("2026-07-24 alice\nnot a date");
await page.getByRole("button", { name: "Add pasted text" }).click();
await page.getByLabel("Add corpus text files").setInputFiles([
{
name: "second.txt",
mimeType: "text/plain",
buffer: Buffer.from("2025-12-31 Bérénice", "utf8"),
},
{
name: "empty.txt",
mimeType: "text/plain",
buffer: Buffer.from("no date here", "utf8"),
},
]);
await expect(
page.getByRole("list", { name: "Corpus documents" }).getByRole("listitem"),
).toHaveCount(3);
await page.getByRole("button", { name: "Scan corpus" }).click();
const results = page.getByRole("table", {
name: "Corpus document results",
});
await expect(results).toBeVisible();
await expect(results.locator("tbody tr")).toHaveCount(3);
await expect(results.locator("tbody tr").nth(0)).toContainText("complete");
await expect(results.locator("tbody tr").nth(0)).toContainText("1");
await expect(results.locator("tbody tr").nth(2)).toContainText("0");
await expect(
page.getByRole("table", {
name: "Capture summaries for pasted-text.txt",
}),
).toContainText("alice");
const reportDownload = page.waitForEvent("download");
await page.getByRole("button", { name: "Download summary JSON" }).click();
const report = await reportDownload;
expect(report.suggestedFilename()).toBe("regex-corpus-report.json");
const reportPath = await report.path();
if (!reportPath) throw new Error("Corpus report download unavailable");
const reportText = await readFile(reportPath, "utf8");
expect(reportText).not.toContain("2026-07-24 alice");
expect(JSON.parse(reportText)).toMatchObject({
schemaVersion: 1,
operation: "match",
status: "complete",
summary: { totalDocuments: 3, totalMatches: 2 },
});
await page.getByRole("radio", { name: /Apply replacement/u }).check();
await page.getByRole("button", { name: "Apply to corpus" }).click();
await expect(results.locator("tbody tr")).toHaveCount(3);
await expect(results.locator("tbody tr").first()).toContainText("complete");
const contentConfirmation = page.getByRole("checkbox", {
name: "I understand applied downloads contain corpus content",
});
await expect(
page.getByRole("button", { name: "Download applied ZIP" }),
).toBeDisabled();
await contentConfirmation.check();
await expect(
page.getByRole("heading", { name: "Replacement preview" }),
).toBeVisible();
await expect(page.locator(".corpus-output-preview pre")).toContainText(
"alice — 2026-07-24",
);
const zipDownload = page.waitForEvent("download");
await page.getByRole("button", { name: "Download applied ZIP" }).click();
const archive = await zipDownload;
expect(archive.suggestedFilename()).toBe("regex-corpus-applied.zip");
const archivePath = await archive.path();
if (!archivePath) throw new Error("Corpus ZIP download unavailable");
const files = unzipSync(await readFile(archivePath));
expect(Object.keys(files).sort()).toEqual([
"empty.replaced.txt",
"pasted-text.replaced.txt",
"second.replaced.txt",
]);
expect(new TextDecoder().decode(files["pasted-text.replaced.txt"])).toContain(
"alice — 2026-07-24",
);
await page.getByRole("button", { name: "Project", exact: true }).click();
const projectDownload = page.waitForEvent("download");
await page
.locator(".project-popover")
.getByRole("button", { name: "Export JSON" })
.click();
const projectFile = await projectDownload;
const projectPath = await projectFile.path();
if (!projectPath) throw new Error("Project download unavailable");
const projectText = await readFile(projectPath, "utf8");
expect(projectText).not.toContain("2026-07-24 alice");
expect(JSON.parse(projectText)).toMatchObject({ mode: "corpus" });
});
test("corpus independent-line mode has explicit anchor semantics and preserves separators", async ({
page,
}) => {
await page.goto("./");
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await setEditor(page, "editor-regular-expression-pattern", "^.+$");
await page
.getByRole("button", { name: "Corpus / Apply", exact: true })
.click();
await page
.getByRole("textbox", { name: "Pasted corpus text" })
.fill("alpha\r\nbeta");
await page.getByRole("button", { name: "Add pasted text" }).click();
await expect(page.getByRole("button", { name: "Scan corpus" })).toBeEnabled();
await page.getByRole("button", { name: "Scan corpus" }).click();
const resultRow = page
.getByRole("table", { name: "Corpus document results" })
.locator("tbody tr")
.first();
await expect(resultRow.locator("td").nth(1)).toHaveText("0");
await page.getByRole("radio", { name: /Independent lines/u }).check();
await expect(page.getByText(/Anchors see one line at a time/u)).toBeVisible();
await page.getByRole("button", { name: "Scan corpus" }).click();
await expect(resultRow.locator("td").nth(1)).toHaveText("2");
await page.getByRole("radio", { name: /Apply replacement/u }).check();
await page
.getByRole("textbox", { name: "Replacement template" })
.fill("[$&]");
await page.getByRole("button", { name: "Apply to corpus" }).click();
await expect(page.locator(".corpus-output-preview pre")).toContainText(
"[alpha]",
);
await expect(page.locator(".corpus-output-preview pre")).toContainText(
"[beta]",
);
expect(await page.locator(".corpus-output-preview pre").textContent()).toBe(
"[alpha]\n[beta]",
);
});
test("a running corpus job can be cancelled and labels unprocessed output", async ({
page,
}) => {
await page.goto("./");
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await setEditor(page, "editor-regular-expression-pattern", "^(a|aa)+$");
await page.getByLabel("Manual limit").selectOption("10000");
await page
.getByRole("button", { name: "Corpus / Apply", exact: true })
.click();
await page
.getByRole("textbox", { name: "Pasted corpus text" })
.fill(`${"a".repeat(48)}!`);
await page.getByRole("button", { name: "Add pasted text" }).click();
await expect(page.getByRole("button", { name: "Scan corpus" })).toBeEnabled();
await page.getByRole("button", { name: "Scan corpus" }).click();
await page.getByRole("button", { name: "Cancel corpus run" }).click();
await expect(
page.getByRole("table", { name: "Corpus document results" }),
).toContainText("cancelled", { timeout: 15_000 });
await expect(
page.getByRole("button", { name: "Download summary JSON" }),
).toBeEnabled();
});
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");
});