Files
regex-tools/tests/browser/workbench.spec.ts

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");
});