diff --git a/e2e/tests/webext-inspected-page.test.ts b/e2e/tests/webext-inspected-page.test.ts new file mode 100644 index 000000000..801213217 --- /dev/null +++ b/e2e/tests/webext-inspected-page.test.ts @@ -0,0 +1,113 @@ +import type { BrowserContext } from 'playwright-core' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { chromium } from 'playwright-core' +import { expect, it } from 'vitest' +import { installInspectedPageRelay } from '../../packages/webext/app/panel/inspected-page-relay' + +it('preserves structured-clone values across the inspected-page extension relay', async () => { + const manifest = JSON.parse(await readFile(new URL('../../packages/webext/manifest.json', import.meta.url), 'utf8')) + const directory = await mkdtemp(join(tmpdir(), 'webext-relay-')) + const server = createServer((_request, response) => { + response.setHeader('Content-Type', 'text/html') + response.end(``) + }) + // Keep production transport settings and permissions; replace presentation + // entry points with a minimal worker so this test needs no UI build. + await writeFile(join(directory, 'manifest.json'), JSON.stringify({ + ...manifest, + action: undefined, + icons: undefined, + devtools_page: undefined, + background: { service_worker: 'background.js' }, + })) + await writeFile(join(directory, 'background.js'), 'chrome.runtime.onInstalled.addListener(() => {})') + await writeFile(join(directory, 'relay.js'), `(${installInspectedPageRelay.toString()})()`) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() + if (!address || typeof address === 'string') + throw new Error('The relay fixture did not acquire a port') + const url = `http://127.0.0.1:${address.port}/` + let context: BrowserContext | undefined + try { + context = await chromium.launchPersistentContext('', { + channel: 'chromium', + headless: true, + args: [`--disable-extensions-except=${directory}`, `--load-extension=${directory}`], + }) + const worker = context.serviceWorkers()[0] ?? await context.waitForEvent('serviceworker') + const page = await context.newPage() + await page.goto(url) + const result = await worker.evaluate(async (url) => { + const [tab] = await chrome.tabs.query({ url }) + const [injection] = await chrome.scripting.executeScript({ + target: { tabId: tab!.id!, frameIds: [0] }, + files: ['relay.js'], + world: 'ISOLATED', + }) + const port = chrome.tabs.connect(tab!.id!, { + documentId: injection!.documentId, + name: 'vite-devtools:inspected-page:roundtrip', + }) + try { + await new Promise(resolve => port.onMessage.addListener(data => data.type === 'ready' && resolve())) + const received = new Promise(resolve => port.onMessage.addListener(data => data.type === 'channel' && resolve(data.data))) + const shared = { value: 42 } + const cycle: { self?: unknown } = {} + cycle.self = cycle + port.postMessage({ + type: 'channel', + data: { + bytes: new Uint8Array([1, 2, 255]), + big: 9007199254740993n, + map: new Map([['shared', shared]]), + set: new Set([shared]), + shared, + cycle, + date: new Date('2026-01-01T00:00:00Z'), + blob: new Blob(['relay'], { type: 'text/plain' }), + }, + }) + const data = await received + return { + bytes: data.bytes instanceof Uint8Array && [...data.bytes], + big: data.big === 9007199254740993n, + map: data.map instanceof Map && data.map.get('shared') === data.shared, + set: data.set instanceof Set && data.set.has(data.shared), + cycle: data.cycle.self === data.cycle, + date: data.date instanceof Date && data.date.toISOString(), + blob: data.blob instanceof Blob && await data.blob.text(), + } + } + finally { + port.disconnect() + } + }, url) + expect(result).toEqual({ + bytes: [1, 2, 255], + big: true, + map: true, + set: true, + cycle: true, + date: '2026-01-01T00:00:00.000Z', + blob: 'relay', + }) + expect(Number.parseInt(manifest.minimum_chrome_version, 10)).toBeGreaterThanOrEqual(148) + } + finally { + await context?.close() + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) + await rm(directory, { recursive: true, force: true }) + } +}) diff --git a/packages/webext/README.md b/packages/webext/README.md index e8f9fabe4..266c9d214 100644 --- a/packages/webext/README.md +++ b/packages/webext/README.md @@ -2,4 +2,13 @@ Browser extension scaffolding for Vite DevTools. Work in progress. +Requires Chrome 148 or later. The extension uses native structured-clone messaging to preserve inspected-page channel values, including typed arrays, maps, big integers, and cyclic objects. + +Run the browser transport regression after installing the test browser: + +```sh +pnpm -C e2e exec playwright-core install chromium +pnpm -C e2e exec vitest run tests/webext-inspected-page.test.ts +``` + 📖 [Documentation](https://devtools.vite.dev/) diff --git a/packages/webext/app/panel/App.vue b/packages/webext/app/panel/App.vue index 09289d97d..34b20fad2 100644 --- a/packages/webext/app/panel/App.vue +++ b/packages/webext/app/panel/App.vue @@ -1,12 +1,16 @@ @@ -62,6 +91,7 @@ onUnmounted(() => {