feat: add multi-engine regex flavour support
This commit is contained in:
304
tests/browser/additional-flavours.spec.ts
Normal file
304
tests/browser/additional-flavours.spec.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
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: "😀 <abc>",
|
||||
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: "(?<word>😀+)",
|
||||
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: "(?<word>[A-Z&&[^AEIOU]]+)",
|
||||
subject: "😀 BCD",
|
||||
expectedMatch: "BCD",
|
||||
utf16Range: "3…6",
|
||||
nativeRange: "2…5 code-point",
|
||||
replacement: "<\\k<word>>|\\&|\\`|\\'",
|
||||
expectedOutput: "😀 <BCD>|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: "😀 <one>",
|
||||
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<word>\\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<word>\\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: "(?<word>\\p{L}+)",
|
||||
subject: "😀 café",
|
||||
expectedMatch: "café",
|
||||
utf16Range: "3…7",
|
||||
nativeRange: "3…7 utf16",
|
||||
replacement: "<${word}>",
|
||||
expectedOutput: "😀 <café>",
|
||||
capabilities: [
|
||||
"System.Text.RegularExpressions",
|
||||
"10.0.10",
|
||||
".NET browser WebAssembly",
|
||||
"CultureInvariant",
|
||||
],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
id: "scala",
|
||||
title: "Scala/JVM compatibility profile",
|
||||
pattern: "(?<word>[a-z]++)",
|
||||
subject: "😀 abc",
|
||||
expectedMatch: "abc",
|
||||
utf16Range: "3…6",
|
||||
nativeRange: "3…6 utf16",
|
||||
replacement: "<$1>",
|
||||
expectedOutput: "😀 <abc>",
|
||||
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",
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user