From 6a94792750932ddc7220673b8b6808d5f02cc089 Mon Sep 17 00:00:00 2001 From: Alex Nahas Date: Mon, 14 Sep 2026 08:10:30 -0700 Subject: [PATCH] feat(extensions): add tested content-script integration guide Document MAIN-world installation, discovery and invocation through the browser's scripting API, and forwarding toolchange into the isolated world as a payload-free DOM event. The guide is explicit that those events are hints rather than an authorization boundary, since MAIN-world code shares the page's globals. The test builds an MV3 extension out of the guide's own JavaScript blocks and runs it against a real served page in Chromium and in stock Firefox over geckodriver, so the documented example cannot drift from working code. Assert the extraction itself, so editing the guide fails here rather than in a poll timeout. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 13 ++- AGENTS.md | 2 +- EXTENSIONS.md | 80 +++++++++++++++ README.md | 2 + TESTING.md | 42 +++++--- extension.test.ts | 201 +++++++++++++++++++++++++++++++++++++ fixtures/server.mjs | 10 +- package.json | 5 +- package.test.mjs | 1 + playwright.config.ts | 2 + pnpm-lock.yaml | 142 ++++++++++++++++++++++++++ tsconfig.test.json | 1 + 12 files changed, 483 insertions(+), 18 deletions(-) create mode 100644 EXTENSIONS.md create mode 100644 extension.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8e40d4..33081fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,18 @@ jobs: - run: npm install --global pnpm@10.14.0 - run: pnpm install --frozen-lockfile - run: pnpm exec playwright install --with-deps chromium firefox webkit + - uses: browser-actions/setup-firefox@0bc507ddf224827e3b1af68e014d5e42ab93e795 # v1.7.2 + id: firefox + with: + firefox-version: latest + # Pinned to the tip of this action's `latest` branch; it publishes no release tags. + - uses: browser-actions/setup-geckodriver@fac5b0424c584257f32cf12c5eb4ba86014c34f8 + with: + geckodriver-version: 0.37.1 + token: ${{ github.token }} - run: pnpm test + env: + FIREFOX_BIN: ${{ steps.firefox.outputs.firefox-path }} - run: pnpm test:package - uses: actions/upload-artifact@v7 if: always() @@ -63,7 +74,7 @@ jobs: python-version: "3.11" - run: npm install --global pnpm@10.14.0 - run: pnpm install --frozen-lockfile - - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd + - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 id: chrome with: chrome-version: canary diff --git a/AGENTS.md b/AGENTS.md index 4329c46..c97ed60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ compatibility belongs here. browser issue, or standards-position discussion alone is not the specification. 4. Add the smallest real-browser regression that demonstrates the behavior. Tests load the built bundle from a real server; do not replace DOM APIs, - page requests, or tool callbacks with mocks. + extension APIs, page requests, or tool callbacks with mocks. ## Validate diff --git a/EXTENSIONS.md b/EXTENSIONS.md new file mode 100644 index 0000000..85c8169 --- /dev/null +++ b/EXTENSIONS.md @@ -0,0 +1,80 @@ +# Add WebMCP to an existing extension + +Install the polyfill in the page's **MAIN** world. An isolated content script has its own JavaScript globals and cannot install or read the page's `document.modelContext`. Keep extension APIs, credentials, and permission decisions in the extension's isolated context. + +## Install at document start + +Copy the built `dist/polyfill.js` into your extension as `webmcp-polyfill.js`. It is minified; `dist/index.js` is the same implementation unminified if you need to read it. Add this entry to your existing manifest, replacing the match pattern with the origins your extension supports: + +```json +{ + "content_scripts": [ + { + "matches": ["https://example.com/*"], + "js": ["webmcp-polyfill.js"], + "world": "MAIN", + "run_at": "document_start" + } + ] +} +``` + +The bundle is self-contained. It needs no web-accessible resources, network requests, MCP server, or extension-specific runtime. Keep top-frame-only injection for the initial document-local implementation. + +Use host match patterns without a port; enforce an exact origin separately where your extension needs that restriction. + +The page still needs the browser prerequisites described in the README, including an origin-keyed agent cluster. Injecting a script cannot supply a missing `Origin-Agent-Cluster` response header or bypass the page's Permissions Policy. + +## Discover and execute from the extension + +A Chromium extension service worker or Firefox background script can use `chrome.scripting.executeScript` in the MAIN world. The browser carries the result back; there is no need to create a page-message request protocol. The extension needs the `scripting` permission and access to the target page through a host permission or `activeTab`. + +```js +const [{ result: tools }] = await chrome.scripting.executeScript({ + target: { tabId }, + world: "MAIN", + func: async () => { + const tools = await document.modelContext.getTools(); + // WindowProxy cannot cross the extension serialization boundary. + return tools.map(({ window, ...metadata }) => metadata); + }, +}); + +const [{ result }] = await chrome.scripting.executeScript({ + target: { tabId }, + world: "MAIN", + args: [toolName, inputObject], + func: async (name, input) => { + const context = document.modelContext; + const tool = (await context.getTools()).find((tool) => tool.name === name); + if (!tool) throw new Error("Tool is no longer available"); + return context.executeTool(tool, input); + }, +}); +``` + +`tabId`, `toolName`, and `inputObject` come from your extension's own UI or agent. Match the target document as well as the tab when retaining a selection across navigation. Re-check availability and permissions before acting. This example assumes the object-input execution API; older native Chrome implementations are outside this package's compatibility scope. + +`AbortSignal` cannot cross `executeScript` arguments, so the example above has no cancellation. The polyfill supports execution signals; to use them, create the `AbortController` inside the MAIN-world function and drive it from your extension's own cancellation path. + +## Observe changes with DOM events + +For live discovery, a MAIN-world content script can listen to the existing `toolchange` event. If your isolated content script needs a notification, forward a payload-free DOM event on the shared document: + +```js +// MAIN world: load after webmcp-polyfill.js. +document.modelContext.addEventListener("toolchange", () => { + document.dispatchEvent(new Event("my-extension:webmcp-tools-changed")); +}); + +// ISOLATED world: use your existing refresh and extension messaging code. +document.addEventListener("my-extension:webmcp-tools-changed", () => { + // Ask the extension to refresh its tool list using the discovery call above. +}); +``` + +Fetch the initial list after your listener is installed. Notifications are hints: page scripts can forge or suppress them. They must never authorize tool execution or privileged extension actions. Treat discovered metadata and results as page-controlled data too. + +Chrome documents [execution worlds and content-script injection](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts) and [the scripting API](https://developer.chrome.com/docs/extensions/reference/api/scripting). MAIN-world code is visible to and affected by the page; DOM events are communication, not an authentication boundary. + +The integration tests run these examples in both Chromium and Firefox. Firefox also exposes the Promise-based `browser.scripting` namespace. See [Mozilla's scripting API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript) for browser-specific result and error handling. diff --git a/README.md b/README.md index 1644019..43417af 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ The implementation follows [draft source `cc45efc`](https://github.com/webmachin The API tracks the draft. Breaking changes ship with notes: in minor releases while the version is 0.x, in majors after 1.0. +For use in an existing extension, see [extension integration](EXTENSIONS.md). + ## Development See [TESTING.md](https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md) for browser setup, test commands, draft alignment, and known limitations. diff --git a/TESTING.md b/TESTING.md index 19e2b40..5856650 100644 --- a/TESTING.md +++ b/TESTING.md @@ -11,26 +11,41 @@ pnpm test pnpm test:package ``` -| Check | What it runs | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `index.test-d.ts` | TypeScript against the built package declarations, including upstream schema inference | -| `index.test.ts` | Built bundle served over HTTP in Chromium (native WebMCP disabled), Firefox, and WebKit; coercion, metadata, events, errors, detached documents | -| `execute.test.ts` | The same three engines: object input, JSON results, cancellation, concurrent calls, and dispatch-time failures | -| `app.test.ts` | A served application, real button interactions, callback side effects, invalid input, unregistration, and reload | -| `native.test.ts` | Real native Chromium registration, then polyfill loading; context and getter identities must survive | -| `pnpm test:package` | Packed tarball installed into a fresh consumer, public type imports, SSR-safe entry points, and package contents | -| `pnpm test:wpt` | Unmodified upstream WPT and IDL in real Chrome Canary, with native WebMCP disabled | +The Firefox extension project additionally requires a stock Firefox and +[geckodriver](https://github.com/mozilla/geckodriver/releases). Put geckodriver +on PATH, or set `GECKODRIVER` to its executable. Set `FIREFOX_BIN` when Firefox +is not discoverable by geckodriver. CI installs both explicitly. No extension +signing preference is disabled: geckodriver installs a temporary development +add-on into a disposable profile. + +For a focused run, use `pnpm build && pnpm exec playwright test --project=extension-firefox` +or `--project=extension-chromium`; every browser test loads the built bundle, so skipping the +build tests the previous one. Missing prerequisites fail that project. + +| Check | What it runs | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `index.test-d.ts` | TypeScript against the built package declarations, including upstream schema inference | +| `index.test.ts` | Built bundle served over HTTP in Chromium (native WebMCP disabled), Firefox, and WebKit; coercion, metadata, events, errors, detached documents | +| `execute.test.ts` | The same three engines: object input, JSON results, cancellation, concurrent calls, and dispatch-time failures | +| `app.test.ts` | A served application, real button interactions, callback side effects, invalid input, unregistration, and reload | +| `native.test.ts` | Real native Chromium registration, then polyfill loading; context and getter identities must survive | +| `extension.test.ts` | Real MV3 extension in Chromium and stock Firefox: MAIN installation, isolated notifications, background scripting calls, page state, re-registration, reload, and an unmatched origin | +| `pnpm test:package` | Packed tarball installed into a fresh consumer, public type imports, SSR-safe entry points, and package contents | +| `pnpm test:wpt` | Unmodified upstream WPT and IDL in real Chrome Canary, with native WebMCP disabled | The fixture server binds 127.0.0.1:8793 and sets the required `Origin-Agent-Cluster` header; Playwright never reuses an existing server, so free that port first. -Loopback HTTP is a secure context; +Geckodriver picks its own loopback port. Loopback HTTP is a secure context; the WPT `non-secure.html` case supplies the nonsecure-origin check. The browser suite uses fresh contexts and real network responses, without route interception, -fake timers, DOM shims, or mocked tool callbacks. +fake timers, DOM shims, or mocked extension APIs. The extension fixture executes +the integration guide's discovery/invocation example directly. Playwright retains failure traces and an HTML report in `playwright-report/`. -Playwright drives its bundled Firefox for page tests. WebKit is additional engine -coverage, not a claim that Safari itself was tested. +Firefox extension runs attach the browser version, a page screenshot, and +geckodriver logs. Page tests use Playwright's bundled Firefox. Extension tests use stock Firefox +over Selenium, because Playwright loads extensions only in Chromium. WebKit is +engine coverage, not a claim that Safari was tested. ## Run upstream WPT @@ -115,6 +130,7 @@ it, and checks all three operations and exception realms. | [Official types](https://github.com/webmachinelearning/webmcp-types) | Public declarations, schema inference, and pending API updates | | [Blink script_tools](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/script_tools/) | Chromium IDL, implementation, tests, and commit-linked bugs | | [Gecko source search](https://searchfox.org/mozilla-central/search?q=ModelContext) and [Mozilla position](https://github.com/mozilla/standards-positions/issues/1412) | Locate Firefox implementation work and discussion; a position is not evidence of shipped support | +| [Firefox extension worlds](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts) and [scripting](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript) | Browser extension integration and serialization boundaries | Reproduce a disagreement before changing code or expectations, and record what changed in the draft, types, WPT, and browser implementation separately. diff --git a/extension.test.ts b/extension.test.ts new file mode 100644 index 0000000..526e2f3 --- /dev/null +++ b/extension.test.ts @@ -0,0 +1,201 @@ +import { test, expect, chromium } from "@playwright/test"; +import { readFile, mkdir, writeFile, copyFile, open } from "node:fs/promises"; +import { By } from "selenium-webdriver"; +import { Driver, Options, ServiceBuilder } from "selenium-webdriver/firefox.js"; + +// This extension is a test fixture only. Its fixed button action is deliberately +// visible on the page; production authorization belongs in the extension's UI. +test("an installed extension discovers and executes real page tools across worlds", async ({ + browserName, + request, +}, testInfo) => { + test.setTimeout(90_000); + const extension = testInfo.outputPath("extension"); + await mkdir(extension, { recursive: true }); + const guide = await readFile(new URL("./EXTENSIONS.md", import.meta.url), "utf8"); + const blocks = [...guide.matchAll(/```js\n([\s\S]*?)```/g)].map((match) => match[1]!); + const marker = "const [{ result }]"; + const isolated = "// ISOLATED world:"; + const guideIs = (n: string, what: string) => `EXTENSIONS.md's ${n} js code block ${what}`; + expect(blocks, guideIs("set of", "must be exactly two: invocation, then notifications")).toHaveLength(2); + const [invocation, notifications] = blocks; + expect(invocation, guideIs("first", "must be the chrome.scripting example")).toContain( + "chrome.scripting.executeScript", + ); + expect(invocation, guideIs("first", `must still split at "${marker}"`)).toContain(marker); + expect(notifications, guideIs("second", `must still contain "${isolated}"`)).toContain(isolated); + const discovery = invocation.slice(0, invocation.indexOf(marker)); + const [main] = notifications.split(isolated); + expect(main, "the MAIN-world half must add a toolchange listener").toContain( + 'addEventListener("toolchange"', + ); + const firefox = browserName === "firefox"; + await Promise.all([ + copyFile(new URL("./dist/polyfill.js", import.meta.url), `${extension}/webmcp-polyfill.js`), + writeFile(`${extension}/main.js`, main), + writeFile( + `${extension}/background.js`, + ` + chrome.runtime.onMessage.addListener((message, sender, respond) => { + if (!sender.tab || !sender.url.startsWith('http://localhost:8793/')) return; + (async () => { + const tabId = sender.tab.id; + if (message.action === 'discover') { + ${discovery} + return { tools }; + } + const toolName = 'increment'; + const inputObject = { amount: 2 }; + ${invocation} + return { tools, result }; + })().then(respond, error => respond({ error: String(error) })); + return true; + }); + `, + ), + writeFile( + `${extension}/isolated.js`, + ` + async function refresh() { + const response = await chrome.runtime.sendMessage({ action: 'discover' }); + document.getElementById('extension-tools').textContent = response.error ?? response.tools.map(tool => tool.name).join(','); + document.documentElement.dataset.toolChangeWorld = typeof document.modelContext; + } + document.addEventListener('my-extension:webmcp-tools-changed', refresh); + document.addEventListener('DOMContentLoaded', refresh); + document.addEventListener('click', async event => { + if (event.target.id !== 'extension-run') return; + const response = await chrome.runtime.sendMessage({ action: 'execute' }); + document.getElementById('extension-result').textContent = response.error ?? response.result; + }); + `, + ), + writeFile( + `${extension}/manifest.json`, + JSON.stringify({ + manifest_version: 3, + name: "WebMCP test fixture", + version: "0.0.0", + permissions: ["scripting"], + host_permissions: ["http://localhost/*"], + background: firefox ? { scripts: ["background.js"] } : { service_worker: "background.js" }, + browser_specific_settings: { gecko: { id: "webmcp-test@example.org" } }, + content_scripts: [ + { + matches: ["http://localhost/*"], + js: ["webmcp-polyfill.js", "main.js"], + world: "MAIN", + run_at: "document_start", + }, + { matches: ["http://localhost/*"], js: ["isolated.js"], run_at: "document_start" }, + ], + }), + ), + ]); + + let navigate: (url: string) => Promise; + let evaluate: (script: string) => Promise; + let click: (id: string) => Promise; + let close: () => Promise; + if (firefox) { + // Selenium owns the Firefox protocol, process, and session lifecycle. + const options = new Options().addArguments("-headless"); + if (process.env.FIREFOX_BIN) options.setBinary(process.env.FIREFOX_BIN); + const log = testInfo.outputPath("geckodriver.log"); + const logFile = await open(log, "w"); + const service = new ServiceBuilder(process.env.GECKODRIVER ?? "geckodriver") + .setStdio(["ignore", logFile.fd, logFile.fd]) + .build(); + const driver = Driver.createSession(options, service); + close = async () => { + try { + await testInfo.attach("page.png", { + body: Buffer.from(await driver.takeScreenshot(), "base64"), + contentType: "image/png", + }); + } finally { + try { + await driver.quit(); + } finally { + await logFile.close(); + await testInfo.attach("geckodriver.log", { path: log, contentType: "text/plain" }); + } + } + }; + try { + const capabilities = await driver.getCapabilities(); + await testInfo.attach("browser.json", { + body: JSON.stringify({ version: capabilities.getBrowserVersion() }), + contentType: "application/json", + }); + // Use Gecko's directory endpoint so both browsers load the same unpacked fixture. + const session = await driver.getSession(); + const install = await request.post( + new URL(`/session/${session.getId()}/moz/addon/install`, await service.address()).href, + { + data: { path: extension, temporary: true }, + }, + ); + await expect(install).toBeOK(); + navigate = (url) => driver.get(url); + evaluate = (script) => driver.executeScript(script); + click = (id) => driver.findElement(By.id(id)).click(); + } catch (error) { + await close(); + throw error; + } + } else { + const context = await chromium.launchPersistentContext(testInfo.outputPath("profile"), { + channel: "chromium", + headless: true, + args: [ + `--disable-extensions-except=${extension}`, + `--load-extension=${extension}`, + "--disable-features=WebMCP,WebMCPTesting", + ], + }); + const page = await context.newPage(); + await testInfo.attach("browser.json", { + body: JSON.stringify({ version: context.browser()?.version() }), + contentType: "application/json", + }); + navigate = async (url) => { + await page.goto(url); + }; + // Both drivers accept a script body; Playwright evaluates an expression. + evaluate = (script) => page.evaluate(`(() => { ${script} })()`); + click = (id) => page.locator(`#${id}`).click(); + close = () => context.close(); + } + try { + const content = (id: string) => + evaluate(`return document.getElementById('${id}')?.textContent`); + await navigate("http://localhost:8793/extension"); + await expect.poll(() => content("status")).toBe("registered"); + await expect.poll(() => content("extension-tools")).toBe("increment"); + expect(await evaluate("return document.documentElement.dataset.toolChangeWorld")).toBe( + "undefined", + ); + await click("extension-run"); + await expect.poll(() => content("extension-result")).toBe('{"count":2}'); + expect(await content("count")).toBe("2"); + await click("unregister"); + await expect.poll(() => content("status")).toBe("unregistered"); + await expect.poll(() => content("extension-tools")).toBe(""); + await click("register"); + await expect.poll(() => content("extension-tools")).toBe("increment"); + await navigate("http://localhost:8793/extension?reload"); + await expect.poll(() => content("extension-tools")).toBe("increment"); + expect(await content("count")).toBe("0"); + await click("extension-run"); + await expect.poll(() => content("extension-result")).toBe('{"count":2}'); + // The fixture's permission is deliberately restricted to localhost. + await navigate("http://127.0.0.1:8793/"); + expect( + await evaluate("return document.title + '|' + ('modelContext' in document)"), + "the polyfill must not be injected on an origin the extension does not match", + ).toBe("WebMCP test|false"); + } finally { + await close(); + } +}); diff --git a/fixtures/server.mjs b/fixtures/server.mjs index 495fbf3..7a03924 100644 --- a/fixtures/server.mjs +++ b/fixtures/server.mjs @@ -8,6 +8,10 @@ const app = `WebMCP counter loading`; +// Only /extension drives these; the extension's isolated content script writes them. +const extensionUI = ` +`; + createServer(async (request, response) => { const path = new URL(request.url, "http://localhost").pathname; response.setHeader("Origin-Agent-Cluster", path === "/no-cluster" ? "?0" : "?1"); @@ -23,10 +27,12 @@ createServer(async (request, response) => { } else if (["/", "/health", "/no-cluster"].includes(path)) { response.setHeader("Content-Type", "text/html"); response.end("WebMCP test"); - } else if (path === "/app") { + } else if (path === "/app" || path === "/extension") { response.setHeader("Content-Type", "text/html"); response.end( - app + '', + app + + (path === "/app" ? '' : extensionUI) + + '', ); } else { response.writeHead(404).end(); diff --git a/package.json b/package.json index c28027a..24d3aae 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "url": "git+https://github.com/webmachinelearning/webmcp-polyfill.git" }, "files": [ - "dist" + "dist", + "EXTENSIONS.md" ], "type": "module", "sideEffects": [ @@ -34,8 +35,10 @@ "devDependencies": { "@playwright/test": "^1.55.0", "@types/node": "^24.13.4", + "@types/selenium-webdriver": "4.35.6", "esbuild": "^0.25.0", "oxlint": "1.82.0", + "selenium-webdriver": "4.49.0", "typescript": "^5.9.0" }, "packageManager": "pnpm@10.14.0", diff --git a/package.test.mjs b/package.test.mjs index 0b4dba2..0f9e9d9 100644 --- a/package.test.mjs +++ b/package.test.mjs @@ -34,6 +34,7 @@ try { .map((entry) => relative(installed, join(entry.parentPath, entry.name)).replaceAll("\\", "/")) .sort(); assert.deepEqual(files, [ + "EXTENSIONS.md", "LICENSE", "README.md", "dist/auto.d.ts", diff --git a/playwright.config.ts b/playwright.config.ts index 3c6d3ec..4da2cda 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,5 +30,7 @@ export default defineConfig({ launchOptions: { args: ["--enable-experimental-web-platform-features"] }, }, }, + { name: "extension-chromium", testMatch: "extension.test.ts" }, + { name: "extension-firefox", testMatch: "extension.test.ts", use: { browserName: "firefox" } }, ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64d1b3c..1135fc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,18 +18,27 @@ importers: '@types/node': specifier: ^24.13.4 version: 24.13.4 + '@types/selenium-webdriver': + specifier: 4.35.6 + version: 4.35.6 esbuild: specifier: ^0.25.0 version: 0.25.12 oxlint: specifier: 1.82.0 version: 1.82.0 + selenium-webdriver: + specifier: 4.49.0 + version: 4.49.0 typescript: specifier: ^5.9.0 version: 5.9.3 packages: + '@bazel/runfiles@6.5.0': + resolution: {integrity: sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -308,11 +317,35 @@ packages: '@types/node@24.13.4': resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} + '@types/selenium-webdriver@4.35.6': + resolution: {integrity: sha512-8nfyMRi4VvkY9QrQGyY/zkleAhnjnmE8YtdEeoCrWe3izp1P9vo9f5VTNRYF0up+l+kn+VuZah+je+bLddNV+g==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jszip@3.10.2: + resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + oxlint@1.82.0: resolution: {integrity: sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -326,6 +359,9 @@ packages: vite-plus: optional: true + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + playwright-core@1.63.0: resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} @@ -336,6 +372,29 @@ packages: engines: {node: '>=20'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + selenium-webdriver@4.49.0: + resolution: {integrity: sha512-16XqeOEMj+4+p+pzLLyvNWmF5jCY05EKIl3e1anZBgv4+zq7PUTtiNjKtQhMpwosZ0wIMKDFAQ2xdyogOHD4DA==} + engines: {node: '>= 22.0.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -344,11 +403,28 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + webmcp-types@0.1.7: resolution: {integrity: sha512-70YszESbCx+ozBejmBPTSc0hMyXim+/Kpj8UOZsHd36ANcYqIfkDbMBTTn5y+tQzQYUYN1x2R+5PseKJtVaLqA==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + snapshots: + '@bazel/runfiles@6.5.0': {} + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -492,6 +568,17 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/selenium-webdriver@4.35.6': + dependencies: + '@types/node': 24.13.4 + '@types/ws': 8.18.1 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.4 + + core-util-is@1.0.3: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -521,6 +608,23 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + immediate@3.0.6: {} + + inherits@2.0.4: {} + + isarray@1.0.0: {} + + jszip@3.10.2: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + oxlint@1.82.0: optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.82.0 @@ -543,14 +647,52 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.82.0 '@oxlint/binding-win32-x64-msvc': 1.82.0 + pako@1.0.11: {} + playwright-core@1.63.0: {} playwright@1.63.0: dependencies: playwright-core: 1.63.0 + process-nextick-args@2.0.1: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + safe-buffer@5.1.2: {} + + selenium-webdriver@4.49.0: + dependencies: + '@bazel/runfiles': 6.5.0 + jszip: 3.10.2 + tmp: 0.2.7 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + setimmediate@1.0.5: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + tmp@0.2.7: {} + typescript@5.9.3: {} undici-types@7.18.2: {} + util-deprecate@1.0.2: {} + webmcp-types@0.1.7: {} + + ws@8.21.3: {} diff --git a/tsconfig.test.json b/tsconfig.test.json index 058e7d9..aff7a62 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -6,6 +6,7 @@ "index.test.ts", "execute.test.ts", "app.test.ts", + "extension.test.ts", "native.test.ts", "playwright.config.ts" ]