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" ]