782 lines
27 KiB
TypeScript
782 lines
27 KiB
TypeScript
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", "(?<word>\\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", "(?<stale>\\w+)");
|
|
await setEditor(page, "editor-replacement-template", "$<stale>");
|
|
await setEditor(
|
|
page,
|
|
"editor-regular-expression-pattern",
|
|
"(?<letter>[A-Z])",
|
|
);
|
|
await setEditor(page, "editor-replacement-template", "[$<letter>]");
|
|
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: "$<letter>" }),
|
|
});
|
|
await expect(currentNamedToken.locator("code")).toHaveText("$<letter>");
|
|
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<HTMLElement>(".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<HTMLElement>(
|
|
`.cm-capture-${index + 1}`,
|
|
);
|
|
if (!capture) throw new Error(`Capture ${index + 1} is unavailable.`);
|
|
return contrast(getComputedStyle(capture).color, editorBackground);
|
|
});
|
|
const success = document.querySelector<HTMLElement>(
|
|
".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");
|
|
});
|