/// import { expect, test, type Page } from "@playwright/test"; import { readdir } from "node:fs/promises"; import path from "node:path"; interface FlavourCase { readonly id: "php" | "perl" | "ruby" | "cpp" | "go" | "rust" | "dotnet" | "scala"; readonly title: string; readonly pattern: string; readonly subject: string; readonly expectedMatch: string; readonly utf16Range: string; readonly nativeRange: string; readonly replacement: string; readonly expectedOutput: string; readonly capabilities: readonly string[]; readonly timeoutMs: number; readonly expectedMatches?: number; } const FLAVOURS: readonly FlavourCase[] = [ { id: "php", title: "PHP preg / PCRE2", pattern: "(?'word'[a-z]++)", subject: "😀 abc", expectedMatch: "abc", utf16Range: "3…6", nativeRange: "5…8 utf8-byte", replacement: "<$1>", expectedOutput: "😀 ", capabilities: [ "PHP preg (PCRE2)", "PHP 8.5.8 preg / PCRE2 10.44", "@php-wasm/web-8-5 3.1.46", ], timeoutMs: 60_000, }, { id: "perl", title: "Perl 5.28.1 via legacy WebPerl", pattern: "(?😀+)", subject: "x😀😀 y😀", expectedMatch: "😀😀", utf16Range: "1…5", nativeRange: "1…3 code-point", replacement: "${word}-$1-$$", expectedOutput: "x😀😀-😀😀-$ y😀-😀-$", capabilities: [ "Perl regular expressions (legacy)", "5.28.1", "WebPerl 0.09-beta", "legacy beta", "classic worker", ], timeoutMs: 60_000, expectedMatches: 2, }, { id: "ruby", title: "CRuby Regexp", pattern: "(?[A-Z&&[^AEIOU]]+)", subject: "😀 BCD", expectedMatch: "BCD", utf16Range: "3…6", nativeRange: "2…5 code-point", replacement: "<\\k>|\\&|\\`|\\'", expectedOutput: "😀 |BCD|😀 |", capabilities: [ "CRuby Regexp via ruby.wasm", "CRuby 4.0.0 Regexp", "ruby.wasm 2.9.3-2.9.4", "wasm32-wasi", ], timeoutMs: 60_000, }, { id: "cpp", title: "C++ libc++ std::regex", pattern: "(\\w+)", subject: "😀 one", expectedMatch: "one", utf16Range: "3…6", nativeRange: "2…5 code-point", replacement: "<$1>", expectedOutput: "😀 ", capabilities: [ "C++ std::wregex (Emscripten libc++)", "Emscripten 6.0.4 libc++ std::wregex", "32-bit wchar_t code-point bridge", ], timeoutMs: 30_000, }, { id: "go", title: "Go standard-library regexp", pattern: "(?P\\p{Greek}+)", subject: "😀 λλ", expectedMatch: "λλ", utf16Range: "3…5", nativeRange: "5…9 utf8-byte", replacement: "<${word}>", expectedOutput: "😀 <λλ>", capabilities: ["Go standard-library regexp", "1.26.5", "go1.26.5 js/wasm"], timeoutMs: 30_000, }, { id: "rust", title: "Rust regex crate", pattern: "(?P\\p{Greek}+)", subject: "😀 λλ", expectedMatch: "λλ", utf16Range: "3…5", nativeRange: "5…9 utf8-byte", replacement: "<${word}>", expectedOutput: "😀 <λλ>", capabilities: [ "Rust regex crate", "1.13.1", "rustc 1.97.1 wasm32-unknown-unknown", ], timeoutMs: 30_000, }, { id: "dotnet", title: ".NET System.Text.RegularExpressions", pattern: "(?\\p{L}+)", subject: "😀 café", expectedMatch: "café", utf16Range: "3…7", nativeRange: "3…7 utf16", replacement: "<${word}>", expectedOutput: "😀 ", capabilities: [ "System.Text.RegularExpressions", "10.0.10", ".NET browser WebAssembly", "CultureInvariant", ], timeoutMs: 60_000, }, { id: "scala", title: "Scala/JVM compatibility profile", pattern: "(?[a-z]++)", subject: "😀 abc", expectedMatch: "abc", utf16Range: "3…6", nativeRange: "3…6 utf16", replacement: "<$1>", expectedOutput: "😀 ", capabilities: [ "Scala/JVM regex via TeaVM java.util.regex", "TeaVM 0.15.0 compatibility profile", "Scala/JVM pattern and replacement semantics", "no Scala runtime", ], timeoutMs: 30_000, }, ]; async function setEditor(page: Page, testId: string, value: string) { await page.getByTestId(testId).locator(".cm-content").fill(value); } async function waitForReady(page: Page, timeout: number) { await expect(page.locator(".run-status")).toHaveClass(/status-ready/u, { timeout, }); } function observeRuntimeErrors(page: Page): string[] { const errors: string[] = []; page.on("pageerror", (error) => errors.push(`page: ${error.message}`)); page.on("console", (message) => { const text = message.text(); if ( message.type() === "error" || /content security policy|\bCSP\b|refused to (?:execute|load)/iu.test(text) ) { errors.push(`console ${message.type()}: ${text}`); } }); return errors; } for (const flavour of FLAVOURS) { test(`${flavour.title} executes native syntax and replacement through its production worker`, async ({ page, }) => { test.setTimeout(flavour.timeoutMs + 60_000); const runtimeErrors = observeRuntimeErrors(page); const response = await page.goto("/deep/nested/regex/"); const csp = response?.headers()["content-security-policy"] ?? ""; expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'"); expect(csp).not.toContain("'unsafe-eval'"); await waitForReady(page, 15_000); await page.getByLabel("Live").uncheck(); await page .getByRole("combobox", { name: "Flavour" }) .selectOption(flavour.id); await setEditor(page, "editor-regular-expression-pattern", flavour.pattern); await setEditor(page, "editor-test-text", flavour.subject); const run = page.getByRole("button", { name: "Run", exact: true }); await expect(run).toBeEnabled({ timeout: 15_000 }); await run.click(); await waitForReady(page, flavour.timeoutMs); await expect(page.locator(".run-status")).toContainText( `native offsets ${flavour.nativeRange.split(" ").at(-1)}`, ); const matches = page.locator('.extraction-row[aria-level="1"]'); if (flavour.expectedMatches !== undefined) { await expect(matches).toHaveCount(flavour.expectedMatches); } const match = matches.first(); await expect(match).toContainText(flavour.expectedMatch); await expect(match).toContainText(`UTF-16 range: ${flavour.utf16Range}`); await expect(match).toContainText(`native range: ${flavour.nativeRange}`); await page .getByRole("button", { name: "Capabilities", exact: true }) .click(); const capabilities = page.getByRole("dialog", { name: "Capabilities" }); for (const identity of flavour.capabilities) { await expect(capabilities).toContainText(identity); } await capabilities .getByRole("button", { name: "Close capabilities" }) .click(); await page.getByRole("button", { name: "Replace", exact: true }).click(); await setEditor(page, "editor-replacement-template", flavour.replacement); await expect(run).toBeEnabled({ timeout: 15_000 }); await run.click(); await waitForReady(page, flavour.timeoutMs); await expect( page.getByTestId("editor-replacement-output").locator(".cm-content"), ).toHaveText(flavour.expectedOutput); expect(runtimeErrors).toEqual([]); }); } test("serves every additional engine module and WebAssembly dependency with production MIME types", async ({ request, }) => { const fixedAssets = [ ["engines/php/php-loader.mjs", "text/javascript"], ["engines/php/php.wasm", "application/wasm"], ["engines/perl/perl-runtime.worker.js", "text/javascript"], ["engines/perl/emperl.js", "text/javascript"], ["engines/perl/emperl.wasm", "application/wasm"], ["engines/ruby/ruby.wasm", "application/wasm"], ["engines/cpp/cpp-regex.mjs", "text/javascript"], ["engines/cpp/cpp-regex.wasm", "application/wasm"], ["engines/go/wasm_exec.mjs", "text/javascript"], ["engines/go/go-regex.wasm", "application/wasm"], ["engines/rust/rust-regex.mjs", "text/javascript"], ["engines/rust/rust-regex_bg.wasm", "application/wasm"], ["engines/dotnet/_framework/dotnet.js", "text/javascript"], ["engines/java/java-regex.mjs", "text/javascript"], ] as const; for (const [asset, expectedMime] of fixedAssets) { const response = await request.get(`./${asset}`); expect(response.ok(), asset).toBe(true); expect(response.headers()["content-type"], asset).toContain(expectedMime); if (expectedMime === "application/wasm") { expect((await response.body()).subarray(0, 4), asset).toEqual( Buffer.from([0x00, 0x61, 0x73, 0x6d]), ); } } const dotnetFramework = path.resolve( "public", "engines", "dotnet", "_framework", ); const hashedDotnetDependencies = (await readdir(dotnetFramework)) .filter((name) => /\.[a-z0-9]{10}\.(?:js|wasm)$/u.test(name)) .sort(); expect(hashedDotnetDependencies.length).toBeGreaterThan(2); expect(hashedDotnetDependencies.some((name) => name.endsWith(".js"))).toBe( true, ); expect(hashedDotnetDependencies.some((name) => name.endsWith(".wasm"))).toBe( true, ); for (const asset of hashedDotnetDependencies) { const response = await request.get(`./engines/dotnet/_framework/${asset}`); expect(response.ok(), asset).toBe(true); expect(response.headers()["content-type"], asset).toContain( asset.endsWith(".wasm") ? "application/wasm" : "text/javascript", ); } });