feat: publish Regex Tools 0.2.0
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
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");
|
||||
@@ -51,6 +55,361 @@ test("standalone ECMAScript workbench parses, explains and extracts", async ({
|
||||
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))-(?<number>\\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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user