From 883658cfe83eadd413368c4a63c4c2a8945b234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Fri, 3 Jul 2026 15:42:09 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20=E6=B7=BB=E5=8A=A0=20Firefox=20MV3?= =?UTF-8?q?=20E2E=20=E8=87=AA=E5=8A=A8=E5=8C=96=E6=B5=8B=E8=AF=95(Selenium?= =?UTF-8?q?=20+=20geckodriver)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playwright 无法渲染 moz-extension:// 扩展 UI 页,故新增独立的 Firefox MV3 E2E 套件,用 Selenium WebDriver + geckodriver 驱动真实 Firefox: - e2e/firefox/:build-ext(dist/ext→dist/firefox 清单变换)、driver (解包安装临时 add-on,规避 zip 导致 content script scripting.js 加载 失败)、mock-server(GM API mock + userscript 补丁)、gm-api-sync(经 真实安装页安装 → 跑 gm_api_sync → 断言)。 - test:e2e:firefox 脚本 + geckodriver / selenium-webdriver 开发依赖。 - playwright.config.ts 忽略 **/firefox/**,该套件不入 Playwright 运行。 - 新增 docs/E2E-FIREFOX.md,并在 DEVELOP / DOC-MAINTENANCE / README 索引。 普通页 29/29,script-src 'none' CSP 页 28/29(GM_addElement 注入内联 script 在 Firefox 不绕过页面 CSP,以 KNOWN_CSP_GAPS 允许项守护)。 --- docs/DEVELOP.md | 1 + docs/DOC-MAINTENANCE.md | 1 + docs/E2E-FIREFOX.md | 122 ++++++++++++++++++++++++ docs/README.md | 1 + e2e/firefox/build-ext.mjs | 75 +++++++++++++++ e2e/firefox/driver.mjs | 124 ++++++++++++++++++++++++ e2e/firefox/gm-api-sync.mjs | 181 ++++++++++++++++++++++++++++++++++++ e2e/firefox/mock-server.mjs | 126 +++++++++++++++++++++++++ package.json | 3 + playwright.config.ts | 4 +- pnpm-lock.yaml | 162 ++++++++++++++++++++++++++++++++ 11 files changed, 799 insertions(+), 1 deletion(-) create mode 100644 docs/E2E-FIREFOX.md create mode 100644 e2e/firefox/build-ext.mjs create mode 100644 e2e/firefox/driver.mjs create mode 100644 e2e/firefox/gm-api-sync.mjs create mode 100644 e2e/firefox/mock-server.mjs diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 9dd3b1ce2..77ed88756 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -81,6 +81,7 @@ Vitest + happy-dom, 850ms timeout. Chrome APIs mocked via `@Packages/chrome-exte - BDD-style Chinese `describe`/`it` titles. Use `describe.concurrent()` / `it.concurrent()` where independent. - Single file: `pnpm test -- --run path/to/file.test.ts`. - Playwright tests are `*.spec.ts` files in `e2e`; they run with one worker and retain failure artifacts. Run targeted tests while iterating, then run `pnpm run lint` plus the relevant full suite before a PR. +- Firefox MV3 has a separate committed E2E suite (Selenium + geckodriver, `e2e/firefox/`) run via `pnpm run test:e2e:firefox` — Playwright cannot drive Firefox extension UI. See [`E2E-FIREFOX.md`](./E2E-FIREFOX.md). ### Writing meaningful tests (what to clean up / not write) diff --git a/docs/DOC-MAINTENANCE.md b/docs/DOC-MAINTENANCE.md index 5ecef8255..1779d8807 100644 --- a/docs/DOC-MAINTENANCE.md +++ b/docs/DOC-MAINTENANCE.md @@ -29,6 +29,7 @@ Aspirational / feature-branch content belongs in that branch's docs, or is clear | [`DEVELOP.md`](./DEVELOP.md) | The concrete "how": commands, structure, style, testing, i18n, commit/PR. | | [`DESIGN.md`](./DESIGN.md) | The design system: light/dark color tokens, theme mechanism, shadcn component palette, layout & responsive patterns, motion, state patterns, new-page recipe. | | [`VERIFICATION.md`](./VERIFICATION.md) | Lightweight end-to-end functional verification — throwaway scratch scripts driving the real built extension. | +| [`E2E-FIREFOX.md`](./E2E-FIREFOX.md) | The committed Firefox MV3 E2E suite (Selenium + geckodriver): how to run it, Firefox-specific hurdles, results. | | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Deep internals: process model, message passing, service/data layers, GM API, execution, build. | | [`translation/README.md`](./translation/README.md) | Translation / localization single source of truth. | | [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md) | This guide: doc-set organization rules + fact-check / anti-drift discipline. | diff --git a/docs/E2E-FIREFOX.md b/docs/E2E-FIREFOX.md new file mode 100644 index 000000000..12b40b90e --- /dev/null +++ b/docs/E2E-FIREFOX.md @@ -0,0 +1,122 @@ +# Firefox MV3 E2E 指南 / Firefox MV3 E2E Guide + +> **What this owns.** The **Firefox MV3** end-to-end suite: how ScriptCat is loaded into a real Firefox and +> driven end-to-end (install a userscript → run it → verify GM APIs), and the Firefox-specific hurdles that make +> it work. The Chrome/Chromium E2E suite (Playwright, `e2e/*.spec.ts`) and the general testing mechanics live in +> [`DEVELOP.md`](./DEVELOP.md) → *Testing*; lightweight throwaway verification lives in +> [`VERIFICATION.md`](./VERIFICATION.md). This is the committed, rerunnable Firefox suite. + +> **Status:** ScriptCat MV3 does not *officially* support Firefox yet (`scripts/pack.js` keeps +> `PACK_FIREFOX = false`). This suite exists to drive the Firefox build end-to-end and guard the parts that +> already work. + +## Why not Playwright + +The Chrome E2E suite drives the extension with Playwright + `--load-extension`. **That does not work for +Firefox:** Playwright's Firefox cannot render *any* `moz-extension://` UI page — `page.goto`, a DNR redirect, +and the extension's own `tabs.create` all leave the tab at `about:blank` (Playwright issues +[#3792](https://github.com/microsoft/playwright/issues/3792) / +[#2644](https://github.com/microsoft/playwright/issues/2644)). Without the extension UI you cannot drive the +install page, so the Firefox suite uses **Selenium WebDriver + geckodriver** (Marionette), which renders +`moz-extension://` pages normally. + +## Prerequisites + +- **A build at `dist/ext`** — run `pnpm run build` (or `pnpm run dev`) first. The runner derives the Firefox + build from it. +- **A real Firefox ≥ 136** on the machine (the MV3 `userScripts` API landed in Firefox 136; the generated + manifest sets `strict_min_version: "136.0"`). geckodriver drives your system Firefox. +- Dev dependencies `selenium-webdriver` + `geckodriver` (already in `package.json`). The `geckodriver` npm + package downloads its binary on demand; no manual install. + +## Running + +```bash +pnpm run build # produce dist/ext (skip if already built) +pnpm run test:e2e:firefox # build dist/firefox, drive Firefox, run gm_api_sync, assert +``` + +Environment flags: + +| Var | Effect | +| --- | --- | +| `HEADED=1` | Show the Firefox window instead of headless. | +| `NO_CSP=1` | Serve the target page **without** CSP; then a clean 29/29 is required (no failures allowed). | + +Exit code `0` = pass. Screenshots are written to `test-results/ff-gm-sync-*.png` (install page, installed list, +result box), which is git-ignored. + +## What it does + +The runner ([`e2e/firefox/gm-api-sync.mjs`](../e2e/firefox/gm-api-sync.mjs)) is the Firefox counterpart of the +Chrome "GM_ sync API tests" in [`e2e/gm-api.spec.ts`](../e2e/gm-api.spec.ts): + +1. **Build the Firefox add-on** ([`build-ext.mjs`](../e2e/firefox/build-ext.mjs)) — copy `dist/ext` → `dist/firefox` + and apply the same manifest transform as `scripts/pack.js` (drop `background.service_worker`, delete + `sandbox`, add `browser_specific_settings.gecko`). +2. **Launch Firefox** ([`driver.mjs`](../e2e/firefox/driver.mjs)) with ScriptCat installed as a temporary add-on. +3. **Start a mock server** ([`mock-server.mjs`](../e2e/firefox/mock-server.mjs)) that serves the target page + (with `script-src 'none'` CSP by default), the mocked GM endpoints (`/get`, `/favicon.ico`, `/bytes/N`, …), + and the patched userscript. The userscript is `example/tests/gm_api_sync_test.js`, patched the same way the + Chrome suite patches it (strip integrity hashes, `jsdelivr`→`unpkg`, rewrite `httpbun`/`@connect` to the + local mock). +4. **Install** the userscript through ScriptCat's **real install page** + (`moz-extension:///src/install.html?url=…`) — click `[data-testid=install-primary]`. +5. **Run** — open the target page, auto-approve the runtime permission prompts (`confirm.html`), and read the + `通过`/`失败` summary the script renders. + +## The one thing that makes it work: install the add-on *unpacked* + +Selenium's `driver.installAddon(dir, true)` **zips** the directory before installing. From a zipped temporary +add-on, Firefox's content process cannot load content-script *source files* — you get +`IPDL protocol Error: Received an invalid file descriptor` → `Unable to load script: .../src/scripting.js`. +`src/scripting.js` is ScriptCat's ISOLATED content bridge (registered via `chrome.scripting.registerContentScripts`); +when it fails to load, the SW ↔ content ↔ inject GM chain never connects and **every userscript silently fails +to run** (no logs, no error). + +The fix ([`driver.mjs`](../e2e/firefox/driver.mjs)): install **unpacked** by POSTing the directory path to +geckodriver's raw endpoint `POST /session/{id}/moz/addon/install` with `{ path: , temporary: true }`. +Inline-code `userScripts.register` (ScriptCat's inject/content bridges) is unaffected — only file-based +`scripting.registerContentScripts` breaks when zipped. + +Other Firefox-specific setup handled by the driver: + +- **Pin the moz-extension UUID** via the `extensions.webextensions.uuids` pref so install/options URLs are known + up front. +- **Pre-grant permissions** by writing `/extension-preferences.json` = `{ "": { "permissions": + ["userScripts"], "origins": [""] } }` so ScriptCat can register userScripts and inject without an + interactive grant. +- **Match pattern** — the Chrome suite keeps `?gm_api_sync` in `@match`; Firefox match patterns do not match the + query string, so the userscript is patched to `@match http://127.0.0.1/*` (scoped by the per-run port). + +## Results & the known CSP gap + +On a normal (non-CSP) page the userscript passes **29/29** — every GM API works on Firefox. On the +`script-src 'none'` CSP page (the faithful, default target) it is **28/29**: the one failure is +`GM_addElement - 创建元素`, because `GM_addElement("script", { textContent })`'s injected inline script does +**not** bypass the page CSP on Firefox (it does on Chrome), so `unsafeWindow.foo === "bar"` fails. + +The runner encodes this as a small allowlist (`KNOWN_CSP_GAPS`) so the suite stays green while still failing on +any **new** regression; if ScriptCat gains CSP bypass on Firefox the set just shrinks. Run with `NO_CSP=1` to +require a clean 29/29. + +## Layout + +``` +e2e/firefox/ + build-ext.mjs # dist/ext → dist/firefox (Firefox manifest transform) + driver.mjs # Selenium + geckodriver launch; unpacked temp-add-on install + mock-server.mjs # GM API mock server + userscript patchers (mirrors e2e/gm-api.spec.ts) + gm-api-sync.mjs # the test: install via UI, run gm_api_sync, assert, screenshot +``` + +The suite is **not** part of the Playwright run (`playwright.config.ts` ignores `**/firefox/**`); it is driven +only by `pnpm run test:e2e:firefox`. + +## Troubleshooting + +- **`Process (pid=…) unexpectedly closed with status 0`** — a transient Firefox launch flake; the driver retries + once. If it persists, kill stray `firefox` / `geckodriver` processes left by interrupted runs. +- **Script installs but never runs** (bare target page, no result box) — almost always the *zipped-add-on* + content-script failure above; confirm the add-on is installed unpacked. +- **`dist/ext not found`** — run `pnpm run build` first. diff --git a/docs/README.md b/docs/README.md index b155b4f26..35525fb4c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ | [`DEVELOP.md`](./DEVELOP.md) | 开发规范:命令、目录结构、编码风格、UI/主题、测试机制、i18n、提交/PR 流程。**写代码前先读。** | | [`DESIGN.md`](./DESIGN.md) | 设计系统参考:深/浅色令牌完整值、主题机制、shadcn 组件清单与选型、布局与响应式范式、动效、状态模式、新建页面配方。**做页面/对话框/区块前先读。** | | [`VERIFICATION.md`](./VERIFICATION.md) | 功能验证指南:用一次性 scratch 脚本驱动真实扩展做端到端验证(不跑全量 E2E、不加永久用例)。**验证改动是否真正跑通时读。** | +| [`E2E-FIREFOX.md`](./E2E-FIREFOX.md) | Firefox MV3 E2E 套件(Selenium+geckodriver):怎么跑、解包安装等 Firefox 专属坑、gm_api_sync 结果与已知 CSP 差异。**动 Firefox e2e 时读。** | | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | 内部原理深入:多进程模型、消息传递、服务/数据层、GM API、脚本执行、构建管线。 | | [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md) | 文档维护与事实核对指南:组织规则、逐条核对清单、一键校验脚本。**改/审文档前先读。** | diff --git a/e2e/firefox/build-ext.mjs b/e2e/firefox/build-ext.mjs new file mode 100644 index 000000000..9a7d86ea5 --- /dev/null +++ b/e2e/firefox/build-ext.mjs @@ -0,0 +1,75 @@ +/* global process */ +// Produce a Firefox-loadable unpacked extension at dist/firefox/ from the Chrome build at +// dist/ext/, applying the same manifest transform as scripts/pack.js: Firefox uses the +// event-page background (scripts), has no offscreen/sandbox pages, and needs +// browser_specific_settings.gecko.id for the temporary add-on install. +// +// Run standalone (`node e2e/firefox/build-ext.mjs`) or import buildFirefoxExt(). +import { promises as fs } from "fs"; +import path from "path"; + +const ROOT = path.resolve(import.meta.dirname, "../.."); +const SRC = path.join(ROOT, "dist/ext"); +const OUT = path.join(ROOT, "dist/firefox"); + +// Beta build (version 1.5.0-beta) → beta gecko id, per scripts/pack.js. +export const GECKO_ID = "{44ab8538-2642-46b0-8a57-3942dbc1a33b}"; +// Pinned moz-extension UUID so options/install URLs are known up front. Firefox reads this +// from the extensions.webextensions.uuids pref set in firefoxUserPrefs at launch. +export const EXT_UUID = "44ab8538-2642-46b0-8a57-3942dbc1a33b"; + +async function copyDir(from, to) { + await fs.mkdir(to, { recursive: true }); + for (const entry of await fs.readdir(from, { withFileTypes: true })) { + const s = path.join(from, entry.name); + const d = path.join(to, entry.name); + if (entry.isDirectory()) await copyDir(s, d); + else await fs.copyFile(s, d); + } +} + +function toFirefoxManifest(manifest) { + const m = { ...manifest, background: { ...manifest.background } }; + // Firefox MV3 uses the event-page background; drop the Chrome service worker. + delete m.background.service_worker; + // Firefox has no chrome.sandbox page mechanism. + delete m.sandbox; + // "background" optional permission is unsupported on Firefox MV3. + m.optional_permissions = (m.optional_permissions || []).filter((p) => p !== "background"); + m.permissions = (m.permissions || []).filter((p) => p !== "background"); + m.browser_specific_settings = { + gecko: { + id: GECKO_ID, + strict_min_version: "136.0", + data_collection_permissions: { + required: ["none"], + optional: ["authenticationInfo", "personallyIdentifyingInfo"], + }, + }, + }; + m.commands = { _execute_action: {} }; + return m; +} + +export async function buildFirefoxExt() { + try { + await fs.access(path.join(SRC, "manifest.json")); + } catch { + throw new Error(`dist/ext not found — run \`pnpm run build\` (or \`pnpm run dev\`) first, then re-run.`); + } + await fs.rm(OUT, { recursive: true, force: true }); + await copyDir(SRC, OUT); + const manifest = JSON.parse(await fs.readFile(path.join(SRC, "manifest.json"), "utf8")); + await fs.writeFile(path.join(OUT, "manifest.json"), JSON.stringify(toFirefoxManifest(manifest), null, 2)); + return OUT; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + buildFirefoxExt().then( + (out) => console.log("[build-ext] wrote", out, "gecko id", GECKO_ID), + (err) => { + console.error(err.message); + process.exit(1); + } + ); +} diff --git a/e2e/firefox/driver.mjs b/e2e/firefox/driver.mjs new file mode 100644 index 000000000..013762ec2 --- /dev/null +++ b/e2e/firefox/driver.mjs @@ -0,0 +1,124 @@ +/* global Buffer */ +// Launch Firefox via Selenium + geckodriver with ScriptCat installed as a temporary MV3 +// add-on. Unlike Playwright's Firefox, geckodriver (Marionette) can render moz-extension:// +// UI pages, so this drives ScriptCat's real install page / options UI. +// +// The add-on is installed UNPACKED (directory) through geckodriver's raw moz/addon/install +// endpoint rather than driver.installAddon(), which zips the directory. From a zipped temp +// add-on Firefox's content process cannot load content-script *source files* ("IPDL protocol +// Error: invalid file descriptor" → "Unable to load script: .../src/scripting.js"), which +// breaks ScriptCat's content bridge and silently stops every userscript from running. +import fs from "fs"; +import os from "os"; +import path from "path"; +import net from "net"; +import http from "http"; +import { Builder } from "selenium-webdriver"; +import firefox from "selenium-webdriver/firefox.js"; +import { createRequire } from "module"; +import { GECKO_ID, EXT_UUID } from "./build-ext.mjs"; + +const require = createRequire(import.meta.url); +const { download, start } = require("geckodriver"); + +const EXT_DIR = path.resolve(import.meta.dirname, "../../dist/firefox"); + +function freeTcpPort() { + return new Promise((resolve) => { + const s = net.createServer(); + s.listen(0, "127.0.0.1", () => { + const p = s.address().port; + s.close(() => resolve(p)); + }); + }); +} + +function installUnpackedAddon(port, sessionId, dir) { + return new Promise((resolve, reject) => { + const data = JSON.stringify({ path: dir, temporary: true }); + const req = http.request( + { + host: "127.0.0.1", + port, + path: `/session/${sessionId}/moz/addon/install`, + method: "POST", + headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) }, + }, + (res) => { + let b = ""; + res.on("data", (c) => (b += c)); + res.on("end", () => + res.statusCode === 200 ? resolve(JSON.parse(b).value) : reject(new Error(`install ${res.statusCode}: ${b}`)) + ); + } + ); + req.on("error", reject); + req.write(data); + req.end(); + }); +} + +export async function launchFirefox({ headless = true } = {}) { + // Profile template pre-granting the MV3 host controls + userScripts optional permission, + // so ScriptCat can register userScripts and inject without an interactive grant. + const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), "ff-sc-prof-")); + fs.writeFileSync( + path.join(profileDir, "extension-preferences.json"), + JSON.stringify({ [GECKO_ID]: { permissions: ["userScripts"], origins: [""] } }) + ); + + const options = new firefox.Options(); + if (headless) options.addArguments("-headless"); + options.setPreference("extensions.manifestV3.enabled", true); + // Pin the internal moz-extension UUID so options/install URLs are known up front. + options.setPreference("extensions.webextensions.uuids", JSON.stringify({ [GECKO_ID]: EXT_UUID })); + options.setPreference("xpinstall.signatures.required", false); + // Extension pages open new tabs; keep them in the same window so Selenium can switch. + options.setPreference("browser.link.open_newwindow", 3); + if (typeof options.setProfile === "function") options.setProfile(profileDir); + + const geckoPath = await download(); + // Retry once: launching a real Firefox occasionally flakes ("Process ... unexpectedly + // closed with status 0") on the first try, especially under load. + let lastErr; + for (let attempt = 1; attempt <= 2; attempt++) { + const port = await freeTcpPort(); + const gecko = await start({ + customGeckoDriverPath: geckoPath, + host: "127.0.0.1", + port, + spawnOpts: { stdio: ["ignore", "ignore", "ignore"] }, + }); + await new Promise((r) => setTimeout(r, 1200)); + try { + const driver = await new Builder() + .usingServer(`http://127.0.0.1:${port}`) + .forBrowser("firefox") + .setFirefoxOptions(options) + .build(); + const sessionId = (await driver.getSession()).getId(); + const addonId = await installUnpackedAddon(port, sessionId, EXT_DIR); + const extUrl = (p) => `moz-extension://${EXT_UUID}/${p.replace(/^\//, "")}`; + const cleanup = async () => { + await driver.quit().catch(() => {}); + try { + gecko.kill(); + } catch { + /* already exited */ + } + fs.rmSync(profileDir, { recursive: true, force: true }); + }; + return { driver, addonId, extUrl, cleanup }; + } catch (e) { + lastErr = e; + try { + gecko.kill(); + } catch { + /* already exited */ + } + await new Promise((r) => setTimeout(r, 1000)); + } + } + fs.rmSync(profileDir, { recursive: true, force: true }); + throw lastErr; +} diff --git a/e2e/firefox/gm-api-sync.mjs b/e2e/firefox/gm-api-sync.mjs new file mode 100644 index 000000000..ddae32077 --- /dev/null +++ b/e2e/firefox/gm-api-sync.mjs @@ -0,0 +1,181 @@ +/* global process */ +// Firefox MV3 end-to-end test: build the Firefox add-on, install +// example/tests/gm_api_sync_test.js into ScriptCat via the real install page, run it +// against a mocked CSP target page, and assert the GM API results — the Firefox +// counterpart of the Chrome "GM_ sync API tests" in e2e/gm-api.spec.ts. +// +// Prerequisite: a build exists at dist/ext (run `pnpm run build` or `pnpm run dev`). +// Run: `pnpm run test:e2e:firefox` (HEADED=1 to watch, NO_CSP=1 to require a clean 29/29). +// Screenshots are saved under test-results/ff-gm-sync-*.png. Exit code 0 = pass. +import fs from "fs"; +import path from "path"; +import { By, until } from "selenium-webdriver"; +import { buildFirefoxExt } from "./build-ext.mjs"; +import { launchFirefox } from "./driver.mjs"; +import { startGMApiMockServer, patchScriptCode, patchTargetMatchCode, patchGMApiTestCode } from "./mock-server.mjs"; + +// Known Firefox-only gap on a `script-src 'none'` CSP page (this test passes on Chrome): +// GM_addElement("script", { textContent }) does not bypass the page CSP on Firefox, so the +// injected inline script does not run and `unsafeWindow.foo === "bar"` fails. Kept as an +// allowlist so the run stays green while still catching any NEW regression; on a non-CSP +// page (NO_CSP=1) nothing is allowed to fail. If ScriptCat gains CSP bypass this set shrinks. +const KNOWN_CSP_GAPS = new Set(["GM_addElement - 创建元素"]); + +const HARD = setTimeout(() => { + console.error("!! HARD TIMEOUT — exiting"); + process.exit(3); +}, 180_000); +HARD.unref?.(); + +const OUT = "test-results"; +const shot = async (driver, name) => { + try { + fs.mkdirSync(OUT, { recursive: true }); + fs.writeFileSync(path.join(OUT, name), await driver.takeScreenshot(), "base64"); + console.log(" screenshot:", path.join(OUT, name)); + } catch (e) { + console.log(" screenshot failed:", e.message); + } +}; + +async function approveConfirmWindows(driver, mainHandle) { + for (const h of await driver.getAllWindowHandles()) { + if (h === mainHandle) continue; + try { + await driver.switchTo().window(h); + if (!(await driver.getCurrentUrl()).includes("confirm.html")) continue; + const request = await driver.findElements(By.css('[data-testid="confirm-request"]')); + if (request.length) { + await request[0].click(); + } else { + const perm = await driver.findElements(By.css('[data-testid="confirm-duration-permanent"]')); + if (perm.length) await perm[0].click().catch(() => {}); + const allow = await driver.findElements(By.css('[data-testid="confirm-allow"]')); + if (allow.length) await allow[0].click(); + } + console.log(" [approve] granted permission on confirm.html"); + } catch { + /* window may have closed mid-iteration */ + } + } + await driver + .switchTo() + .window(mainHandle) + .catch(() => {}); +} + +async function main() { + await buildFirefoxExt(); + + // Prepare the userscript with the same transforms the Chrome suite applies, plus a tiny + // non-invasive instrumentation that records each failing test's name to a DOM attribute + // (does not change the pass/fail counts) so we can assert against the allowlist above. + const raw = fs.readFileSync(path.resolve(import.meta.dirname, "../../example/tests/gm_api_sync_test.js"), "utf8"); + const server = await startGMApiMockServer(); + const targetUrl = `${server.origin}/?gm_api_sync`; + let code = patchScriptCode(raw); + code = patchTargetMatchCode(code, targetUrl); + code = patchGMApiTestCode(code, server.origin); + code = code.replaceAll( + "testResults.failed++;", + "testResults.failed++;try{document.documentElement.setAttribute('data-scfails',(document.documentElement.getAttribute('data-scfails')||'')+name+' | ')}catch(e){}" + ); + server.setUserScript(code); + console.log("mock origin:", server.origin, "| CSP:", process.env.NO_CSP === "1" ? "off" : "on"); + + const { driver, extUrl, cleanup } = await launchFirefox({ headless: process.env.HEADED !== "1" }); + try { + // Install via ScriptCat's real install page (web-accessible; reads ?url= then fetches). + console.log("installing via install page ..."); + await driver.get(`${extUrl("src/install.html")}?url=${server.origin}/gm_api_sync.user.js`); + const primary = await driver.wait(until.elementLocated(By.css('[data-testid="install-primary"]')), 30_000); + await driver.wait(until.elementIsEnabled(primary), 20_000); + await shot(driver, "ff-gm-sync-1-install.png"); + await primary.click(); + await driver.sleep(2500); + + // The install page discards its own tab after installing — move to a valid window. + const afterInstall = await driver.getAllWindowHandles(); + await driver.switchTo().window(afterInstall[afterInstall.length - 1]); + await driver.switchTo().newWindow("tab"); + + // Confirm the script registered (options list shows it). + await driver.get(extUrl("src/options.html")); + await driver.executeScript("try{localStorage.setItem('firstUse','false')}catch(e){}"); + await driver.navigate().refresh(); + await driver.wait( + until.elementLocated(By.css('[data-testid="view-toggle"], [data-testid="mobile-search"]')), + 25_000 + ); + await driver.sleep(1500); + const listText = await driver.executeScript("return document.body.innerText"); + if (!/GM API/i.test(listText)) throw new Error("installed script did not appear in the options list"); + await shot(driver, "ff-gm-sync-2-installed-list.png"); + + // Open the target page in a new tab; the script injects and runs there. + await driver.switchTo().newWindow("tab"); + const mainHandle = await driver.getWindowHandle(); + console.log("running on target:", targetUrl); + await driver.get(targetUrl); + + // Interleave: approve runtime permission prompts + poll the page for the summary. + let passed = -1; + let failed = -1; + const deadline = Date.now() + 90_000; + while (Date.now() < deadline) { + await approveConfirmWindows(driver, mainHandle); + const text = await driver.executeScript("return document.body ? document.body.innerText : ''").catch(() => ""); + const p = String(text).match(/通过[::]\s*(\d+)/); + const f = String(text).match(/失败[::]\s*(\d+)/); + if (p) passed = parseInt(p[1], 10); + if (f) failed = parseInt(f[1], 10); + if (passed >= 0 && failed >= 0) break; + await driver.sleep(500); + } + + // Settle, then re-read the counts and the recorded failure names definitively. + await driver.sleep(1500); + const finalText = await driver.executeScript("return document.body ? document.body.innerText : ''").catch(() => ""); + const total = Number(String(finalText).match(/总测试数[::]\s*(\d+)/)?.[1] ?? -1); + passed = Number(String(finalText).match(/通过[::]\s*(\d+)/)?.[1] ?? passed); + failed = Number(String(finalText).match(/失败[::]\s*(\d+)/)?.[1] ?? failed); + if (failed < 0 && total >= 0 && passed >= 0) failed = total - passed; + await shot(driver, "ff-gm-sync-3-result.png"); + + const recorded = await driver + .executeScript("return document.documentElement.getAttribute('data-scfails')") + .catch(() => null); + const failedNames = String(recorded || "") + .split(" | ") + .map((s) => s.trim()) + .filter(Boolean); + const allow = process.env.NO_CSP === "1" ? new Set() : KNOWN_CSP_GAPS; + const unexpected = failedNames.filter((n) => !allow.has(n)); + + console.log("\n==== gm_api_sync (Firefox MV3) ===="); + console.log(`总测试数: ${total} | 通过: ${passed} | 失败: ${failed}`); + if (failedNames.length) console.log("failing test(s):", failedNames.join(", ")); + if (failedNames.length && !unexpected.length) { + console.log("(all failures are known Firefox CSP gaps — allowed)"); + } + + const ok = passed > 0 && total > 0 && unexpected.length === 0; + if (ok) { + console.log("RESULT: PASS ✅"); + } else { + console.log("RESULT: FAIL ❌"); + if (!(passed > 0 && total > 0)) console.log(" reason: script did not run to completion (no results parsed)"); + if (unexpected.length) console.log(" unexpected failing test(s):", unexpected.join(", ")); + } + await server.close(); + process.exitCode = ok ? 0 : 1; + } finally { + await cleanup(); + clearTimeout(HARD); + } +} + +main().catch((e) => { + console.error("FATAL:", e); + process.exit(4); +}); diff --git a/e2e/firefox/mock-server.mjs b/e2e/firefox/mock-server.mjs new file mode 100644 index 000000000..bd8a0fef3 --- /dev/null +++ b/e2e/firefox/mock-server.mjs @@ -0,0 +1,126 @@ +/* global process, Buffer */ +// GM API mock server + script patchers, mirrored from e2e/gm-api.spec.ts so the Firefox +// (Selenium/geckodriver) run exercises the SAME userscript under the SAME mocked endpoints +// as the Chrome Playwright suite. Firefox has no --host-resolver-rules, so the target host +// is 127.0.0.1 and the CSP header is applied to the target HTML page directly (NO_CSP=1 +// disables it). The userscript itself is served so ScriptCat's install page can fetch it. +import { createServer } from "http"; + +export function startGMApiMockServer() { + return new Promise((resolve, reject) => { + const server = createServer((req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + const url = new URL(req.url || "/", `http://${req.headers.host}`); + + if (url.pathname === "/get") { + res.writeHead(200, { "Content-Type": "application/json" }); + const args = Object.fromEntries(url.searchParams.entries()); + res.end(JSON.stringify({ url: `http://${req.headers.host}${url.pathname}`, args })); + return; + } + if (url.pathname === "/repos/scriptscat/scriptcat") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ name: "scriptcat", full_name: "scriptscat/scriptcat", description: "ScriptCat" })); + return; + } + if (url.pathname === "/favicon.ico") { + res.writeHead(200, { "Content-Type": "image/png" }); + res.end( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", + "base64" + ) + ); + return; + } + const bytesMatch = url.pathname.match(/^\/bytes\/(\d+)$/); + if (bytesMatch) { + res.writeHead(200, { "Content-Type": "application/octet-stream" }); + res.end(Buffer.alloc(Number(bytesMatch[1]), "a")); + return; + } + const delayMatch = url.pathname.match(/^\/delay\/(\d+)$/); + if (delayMatch) { + setTimeout( + () => { + if (res.destroyed) return; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ url: `http://${req.headers.host}${url.pathname}` })); + }, + Number(delayMatch[1]) * 1000 + ); + return; + } + // Serve the userscript so ScriptCat's install page can fetch it via ?url=. + if (typeof server._userScript === "string" && url.pathname.endsWith(".user.js")) { + res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" }); + res.end(server._userScript); + return; + } + // Main target HTML page — apply the same script-src 'none' CSP the Chrome suite uses, + // so ScriptCat's CSP-bypassing injection (GM_addStyle/GM_addElement) is exercised. + if (process.env.NO_CSP !== "1") { + res.setHeader( + "Content-Security-Policy", + "default-src 'none'; script-src 'none'; style-src 'none'; img-src 'self'; connect-src 'self'" + ); + } + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end( + 'ScriptCat E2E
ScriptCat E2E
' + ); + }); + + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const port = server.address().port; + resolve({ + origin: `http://127.0.0.1:${port}`, + port, + setUserScript: (code) => { + server._userScript = code; + }, + close: () => new Promise((res) => server.close(() => res())), + }); + }); + }); +} + +// --- script patchers (mirroring e2e/gm-api.spec.ts) --- + +export function patchScriptCode(code) { + return code + .replace(/^(\/\/\s*@(?:require|resource)\s+.*?)#sha(?:256|384|512)[=-][^\s]+/gm, "$1") + .replace(/https:\/\/cdn\.jsdelivr\.net\/npm\//g, "https://unpkg.com/"); +} + +export function patchTargetMatchCode(code, targetUrl) { + const url = new URL(targetUrl); + // Firefox match patterns do not match the query string in the path glob, so unlike the + // Chrome suite (which keeps `?gm_api_sync`) we match the whole host path. The dedicated + // per-run 127.0.0.1: origin keeps this scoped to just the target page. + const targetPattern = `${url.protocol}//${url.hostname}/*`; + return code.replace( + /^\/\/\s*@match\s+.*\?(gm_api_sync|gm_api_async|inject_content|WINDOW_MESSAGE_TEST_SC|SANDBOX_TEST_SC|unwrap_e2e_test)$/gm, + `// @match ${targetPattern}` + ); +} + +export function patchGMApiTestCode(code, mockOrigin) { + const mockHost = new URL(mockOrigin).host; + return code + .replace(/^\/\/\s*@connect\s+api\.github\.com$/gm, `// @connect 127.0.0.1`) + .replace(/^\/\/\s*@connect\s+httpbun\.com$/gm, `// @connect 127.0.0.1`) + .replace(/https:\/\/api\.github\.com\/repos\/scriptscat\/scriptcat/g, `${mockOrigin}/repos/scriptscat/scriptcat`) + .replace(/https:\/\/httpbun\.com\/get/g, `${mockOrigin}/get`) + .replace(/https:\/\/httpbun\.com\/bytes\/64/g, `${mockOrigin}/bytes/64`) + .replace(/https:\/\/httpbun\.com\/delay\/5/g, `${mockOrigin}/delay/5`) + .replace(/https:\/\/www\.tampermonkey\.net\/favicon\.ico/g, `${mockOrigin}/favicon.ico`) + .replace(/api\.github\.com\/repos\/scriptscat\/scriptcat/g, `${mockHost}/repos/scriptscat/scriptcat`) + .replace(/httpbun\.com\/get/g, `${mockHost}/get`); +} diff --git a/package.json b/package.json index 099bbd312..a7583862d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:e2e:install": "pnpm exec playwright install chromium", "test:e2e": "pnpm exec playwright test", "test:e2e:ui": "pnpm exec playwright test --ui", + "test:e2e:firefox": "node ./e2e/firefox/gm-api-sync.mjs", "validate:yaml": "node ./scripts/validate-yaml.mjs", "validate:yaml:all": "node ./scripts/validate-yaml.mjs --all" }, @@ -91,6 +92,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-userscripts": "^0.5.6", + "geckodriver": "^6.1.0", "globals": "^16.5.0", "happy-dom": "^20.10.2", "husky": "^9.1.7", @@ -99,6 +101,7 @@ "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "prettier": "^3.6.2", + "selenium-webdriver": "^4.45.0", "semver": "^7.7.1", "tailwindcss": "^4.2.4", "tw-animate-css": "^1.4.0", diff --git a/playwright.config.ts b/playwright.config.ts index b4e62bfa1..1e20bd47d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -4,7 +4,9 @@ export default defineConfig({ testDir: "./e2e", // 一次性验证脚本放在 e2e/scratch/(已 gitignore),不纳入正式 E2E 套件/CI。 // 单跑请用 playwright.scratch.config.ts:见 docs/VERIFICATION.md。 - testIgnore: ["**/scratch/**"], + // e2e/firefox/ 是独立的 Selenium+geckodriver Firefox MV3 套件(非 Playwright), + // 用 `pnpm run test:e2e:firefox` 运行:见 docs/E2E-FIREFOX.md。 + testIgnore: ["**/scratch/**", "**/firefox/**"], timeout: 60_000, expect: { timeout: 10_000, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16db5b47c..0bfc0bb73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: eslint-plugin-userscripts: specifier: ^0.5.6 version: 0.5.6(eslint@9.39.2(jiti@2.6.1)) + geckodriver: + specifier: ^6.1.0 + version: 6.1.0 globals: specifier: ^16.5.0 version: 16.5.0 @@ -216,6 +219,9 @@ importers: prettier: specifier: ^3.6.2 version: 3.6.2 + selenium-webdriver: + specifier: ^4.45.0 + version: 4.45.0 semver: specifier: ^7.7.1 version: 7.7.2 @@ -346,6 +352,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bazel/runfiles@6.5.0': + resolution: {integrity: sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -2096,6 +2105,14 @@ packages: '@vitest/utils@4.1.8': resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@wdio/logger@9.18.0': + resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==} + engines: {node: '>=18.20.0'} + + '@zip.js/zip.js@2.8.26': + resolution: {integrity: sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==} + engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2114,6 +2131,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2520,6 +2541,10 @@ packages: supports-color: optional: true + decamelize@6.0.1: + resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2911,6 +2936,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + geckodriver@6.1.0: + resolution: {integrity: sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==} + engines: {node: '>=20.0.0'} + hasBin: true + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3071,6 +3101,10 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-proxy-middleware@2.0.9: resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} engines: {node: '>=12.0.0'} @@ -3084,6 +3118,10 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -3116,6 +3154,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -3362,6 +3403,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3379,6 +3423,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -3481,6 +3528,13 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loglevel-plugin-prefix@0.8.4: + resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} + + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3719,6 +3773,10 @@ packages: resolution: {integrity: sha512-2ORxRN+h40+3/Ylw9LKOtYGfQIoX6grGQlmbvMKqaeZ5/l7oeMvqdJxyG/ax3Poy7VbqMTADI6BwTmO7u10Wrw==} engines: {node: '>=16.0.0'} + modern-tar@0.7.6: + resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} + engines: {node: '>=18.0.0'} + monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} @@ -3857,6 +3915,9 @@ packages: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4154,6 +4215,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -4195,6 +4260,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4208,6 +4277,10 @@ packages: select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + selenium-webdriver@4.45.0: + resolution: {integrity: sha512-Cb2nqvJiwXVOtRTCYHX9D1FJR5+Ls7aL3Nev0t6n4CpXsQ//YGiiUmSCbvTDDeLtbV85SZ46qmLab4SIYKXWRw==} + engines: {node: '>= 20.0.0'} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -4248,6 +4321,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -4440,6 +4516,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4995,6 +5075,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bazel/runfiles@6.5.0': {} + '@bcoe/v8-coverage@1.0.2': {} '@buttercup/fetch@0.2.1': @@ -6748,6 +6830,16 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@wdio/logger@9.18.0': + dependencies: + chalk: 5.6.2 + loglevel: 1.9.2 + loglevel-plugin-prefix: 0.8.4 + safe-regex2: 5.1.1 + strip-ansi: 7.2.0 + + '@zip.js/zip.js@2.8.26': {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6763,6 +6855,8 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -7215,6 +7309,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@6.0.1: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -7732,6 +7828,17 @@ snapshots: functions-have-names@1.2.3: {} + geckodriver@6.1.0: + dependencies: + '@wdio/logger': 9.18.0 + '@zip.js/zip.js': 2.8.26 + decamelize: 6.0.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + modern-tar: 0.7.6 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -7934,6 +8041,13 @@ snapshots: http-parser-js@0.5.10: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + http-proxy-middleware@2.0.9(@types/express@4.17.25): dependencies: '@types/http-proxy': 1.17.17 @@ -7954,6 +8068,13 @@ snapshots: transitivePeerDependencies: - debug + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + husky@9.1.7: {} hyperdyperid@1.2.0: {} @@ -7972,6 +8093,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -8192,6 +8315,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8212,6 +8342,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -8281,6 +8415,10 @@ snapshots: lodash@4.18.1: {} + loglevel-plugin-prefix@0.8.4: {} + + loglevel@1.9.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -8731,6 +8869,8 @@ snapshots: mock-xmlhttprequest@8.4.1: {} + modern-tar@0.7.6: {} + monaco-editor@0.52.2: {} mrmime@2.0.0: {} @@ -8862,6 +9002,8 @@ snapshots: is-network-error: 1.3.0 retry: 0.13.1 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9236,6 +9378,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + ret@0.5.0: {} + retry@0.13.1: {} reusify@1.0.4: {} @@ -9304,6 +9448,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + safer-buffer@2.1.2: {} scheduler@0.27.0: {} @@ -9317,6 +9465,16 @@ snapshots: select-hose@2.0.0: {} + selenium-webdriver@4.45.0: + dependencies: + '@bazel/runfiles': 6.5.0 + jszip: 3.10.1 + tmp: 0.2.7 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.14 @@ -9389,6 +9547,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.0.0 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -9609,6 +9769,8 @@ snapshots: tinyrainbow@3.1.0: {} + tmp@0.2.7: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0