diff --git a/packages/brow-2-brow/package.json b/packages/brow-2-brow/package.json index f431495cb..dbf420ed2 100644 --- a/packages/brow-2-brow/package.json +++ b/packages/brow-2-brow/package.json @@ -13,7 +13,7 @@ "build:dev": "mkdir -p dist && ln -fs ../src/index.html dist/index.html", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/brow-2-brow", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/cli/package.json b/packages/cli/package.json index 85451356f..df380b7aa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,7 +18,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/cli", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/create-package/package.json b/packages/create-package/package.json index 2f9281791..739eddaad 100644 --- a/packages/create-package/package.json +++ b/packages/create-package/package.json @@ -31,7 +31,7 @@ "scripts": { "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/create-package", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/extension/package.json b/packages/extension/package.json index bac0ca824..ccff569f1 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -21,7 +21,7 @@ "build:browser": "OPEN_BROWSER=true yarn build:dev --watch", "build:vite": "vite build --configLoader runner --config vite.config.ts", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/extension", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", @@ -63,7 +63,7 @@ "@ocap/cli": "workspace:^", "@ocap/kernel-test": "workspace:^", "@ocap/repo-tools": "workspace:^", - "@playwright/test": "^1.55.1", + "@playwright/test": "^1.57.0", "@testing-library/jest-dom": "^6.6.3", "@types/chrome": "^0.0.313", "@types/react": "^18.3.18", @@ -83,7 +83,7 @@ "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-promise": "^7.2.1", "jsdom": "^27.4.0", - "playwright": "^1.55.1", + "playwright": "^1.57.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", "tsx": "^4.20.6", diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index b1a11267c..2893e3062 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -4,6 +4,8 @@ import { makeCapTPNotification, isCapTPNotification, getCapTPMessage, + isConsoleForwardMessage, + handleConsoleForwardMessage, } from '@metamask/kernel-browser-runtime'; import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; @@ -107,9 +109,11 @@ async function main(): Promise { const kernelP = backgroundCapTP.getKernel(); globalThis.kernel = kernelP; - // Handle incoming CapTP messages from the kernel + // Handle incoming messages from offscreen (CapTP and console-forward) const drainPromise = offscreenStream.drain((message) => { - if (isCapTPNotification(message)) { + if (isConsoleForwardMessage(message)) { + handleConsoleForwardMessage(message); + } else if (isCapTPNotification(message)) { const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); } else { diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index c09ec2772..2d5dd2f2c 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -2,6 +2,8 @@ import { makeIframeVatWorker, PlatformServicesServer, createRelayQueryString, + setupConsoleForwarding, + isConsoleForwardMessage, } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; @@ -31,6 +33,18 @@ async function main(): Promise { JsonRpcMessage >(chrome.runtime, 'offscreen', 'background', isJsonRpcMessage); + // Set up console forwarding to background for Playwright capture + setupConsoleForwarding(backgroundStream, 'offscreen'); + + // Listen for console messages from vat iframes and forward to background + window.addEventListener('message', (event) => { + if (isConsoleForwardMessage(event.data)) { + backgroundStream.write(event.data).catch(() => { + // Ignore errors if stream isn't ready + }); + } + }); + const kernelStream = await makeKernelWorker(); // Handle messages from the background script / kernel diff --git a/packages/extension/test/helpers.ts b/packages/extension/test/helpers.ts index f7ff7a931..fe6dd63a4 100644 --- a/packages/extension/test/helpers.ts +++ b/packages/extension/test/helpers.ts @@ -11,7 +11,7 @@ const extensionPath = path.resolve( ); export const loadExtension = async (contextId?: string) => { - return makeLoadExtension({ + const result = await makeLoadExtension({ contextId, extensionPath, onPageLoad: async (popupPage) => { @@ -21,4 +21,13 @@ export const loadExtension = async (contextId?: string) => { ).toBeVisible(); }, }); + + // Wrap browserContext.close to auto-attach logs + const originalClose = result.browserContext.close.bind(result.browserContext); + result.browserContext.close = async () => { + await result.attachLogs(); + return originalClose(); + }; + + return result; }; diff --git a/packages/kernel-agents-repl/package.json b/packages/kernel-agents-repl/package.json index bc217fd60..9a027f43b 100644 --- a/packages/kernel-agents-repl/package.json +++ b/packages/kernel-agents-repl/package.json @@ -32,7 +32,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel-agents-repl", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-agents/package.json b/packages/kernel-agents/package.json index c5ce31169..59432ea55 100644 --- a/packages/kernel-agents/package.json +++ b/packages/kernel-agents/package.json @@ -132,7 +132,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel-agents", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 89597fa7b..c92b5c998 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -48,7 +48,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-browser-runtime", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-browser-runtime", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index f52b98667..0ed42d670 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -11,7 +11,9 @@ describe('index', () => { 'createRelayQueryString', 'getCapTPMessage', 'getRelaysFromCurrentLocation', + 'handleConsoleForwardMessage', 'isCapTPNotification', + 'isConsoleForwardMessage', 'makeBackgroundCapTP', 'makeCapTPNotification', 'makeIframeVatWorker', @@ -19,6 +21,9 @@ describe('index', () => { 'receiveInternalConnections', 'rpcHandlers', 'rpcMethodSpecs', + 'setupConsoleForwarding', + 'setupPostMessageConsoleForwarding', + 'stringifyConsoleArg', ]); }); }); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index ecc9b8ad8..f1bace721 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -17,6 +17,7 @@ import { import type { CapTPMessage } from '../background-captp.ts'; import { receiveInternalConnections } from '../internal-comms/internal-connections.ts'; import { PlatformServicesClient } from '../PlatformServicesClient.ts'; +import { setupConsoleForwarding } from '../utils/console-forwarding.ts'; import { makeKernelCapTP } from './captp/index.ts'; import { makeLoggingMiddleware } from './middleware/logging.ts'; import { makePanelMessageMiddleware } from './middleware/panel-message.ts'; @@ -46,6 +47,9 @@ async function main(): Promise { makeSQLKernelDatabase({ dbFilename: DB_FILENAME }), ]); + // Set up console forwarding - messages flow through offscreen to background + setupConsoleForwarding(messageStream, 'kernel-worker'); + const resetStorage = new URLSearchParams(globalThis.location.search).get('reset-storage') === 'true'; diff --git a/packages/kernel-browser-runtime/src/utils/console-forwarding.test.ts b/packages/kernel-browser-runtime/src/utils/console-forwarding.test.ts new file mode 100644 index 000000000..901d8a0a9 --- /dev/null +++ b/packages/kernel-browser-runtime/src/utils/console-forwarding.test.ts @@ -0,0 +1,278 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { + isConsoleForwardMessage, + stringifyConsoleArg, + setupConsoleForwarding, + setupPostMessageConsoleForwarding, + handleConsoleForwardMessage, +} from './console-forwarding.ts'; +import type { ConsoleForwardMessage } from './console-forwarding.ts'; + +// Mock harden to do nothing since we're not in SES +vi.stubGlobal( + 'harden', + vi.fn((obj: unknown) => obj), +); + +describe('console-forwarding', () => { + describe('isConsoleForwardMessage', () => { + it('returns true for valid console-forward message', () => { + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'offscreen', + method: 'log', + args: ['test'], + }, + }; + expect(isConsoleForwardMessage(message)).toBe(true); + }); + + it.each([ + { name: 'null', value: null }, + { name: 'undefined', value: undefined }, + { name: 'string', value: 'test' }, + { name: 'number', value: 123 }, + { name: 'array', value: [] }, + { name: 'object without method', value: { jsonrpc: '2.0' } }, + { name: 'object with different method', value: { method: 'other' } }, + ])('returns false for $name', ({ value }) => { + expect(isConsoleForwardMessage(value)).toBe(false); + }); + }); + + describe('stringifyConsoleArg', () => { + it.each([ + { name: 'string', input: 'hello', expected: 'hello' }, + { name: 'number', input: 42, expected: '42' }, + { name: 'boolean true', input: true, expected: 'true' }, + { name: 'boolean false', input: false, expected: 'false' }, + { name: 'null', input: null, expected: 'null' }, + { name: 'undefined', input: undefined, expected: undefined }, + { name: 'object', input: { foo: 'bar' }, expected: '{"foo":"bar"}' }, + { name: 'array', input: [1, 2, 3], expected: '[1,2,3]' }, + { + name: 'nested object', + input: { a: { b: 1 } }, + expected: '{"a":{"b":1}}', + }, + ])('stringifies $name correctly', ({ input, expected }) => { + expect(stringifyConsoleArg(input)).toBe(expected); + }); + }); + + describe('setupConsoleForwarding', () => { + let originalConsole: typeof console; + let mockStream: { + write: ReturnType; + }; + + beforeEach(() => { + originalConsole = { ...console }; + mockStream = { + write: vi.fn().mockResolvedValue(undefined), + }; + }); + + afterEach(() => { + // Restore original console methods + Object.assign(console, originalConsole); + }); + + it('wraps all console methods', () => { + setupConsoleForwarding(mockStream as never, 'test-source'); + + const methods = ['log', 'debug', 'info', 'warn', 'error'] as const; + for (const method of methods) { + expect(console[method]).not.toBe(originalConsole[method]); + } + }); + + it.each(['log', 'debug', 'info', 'warn', 'error'] as const)( + 'forwards %s method to stream with source', + (method) => { + setupConsoleForwarding(mockStream as never, 'test-source'); + + console[method]('test message', 123); + + expect(mockStream.write).toHaveBeenCalledWith({ + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'test-source', + method, + args: ['test message', '123'], + }, + }); + }, + ); + + it('calls original console method', () => { + // Spy on console.log BEFORE setupConsoleForwarding captures it + const originalLog = vi.spyOn(console, 'log'); + setupConsoleForwarding(mockStream as never, 'test-source'); + + console.log('test'); + + expect(originalLog).toHaveBeenCalledWith('test'); + originalLog.mockRestore(); + }); + + it('ignores stream write errors', async () => { + mockStream.write.mockRejectedValue(new Error('Stream not ready')); + setupConsoleForwarding(mockStream as never, 'test-source'); + + // Should not throw + + expect(() => console.log('test')).not.toThrow(); + }); + }); + + describe('handleConsoleForwardMessage', () => { + let consoleSpy: ReturnType; + + afterEach(() => { + consoleSpy?.mockRestore(); + }); + + it.each(['log', 'debug', 'info', 'warn', 'error'] as const)( + 'calls console.%s with source prefix and args', + (method) => { + consoleSpy = vi + .spyOn(console, method) + .mockImplementation(() => undefined); + + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'offscreen', + method, + args: ['arg1', 'arg2'], + }, + }; + + handleConsoleForwardMessage(message); + + expect(consoleSpy).toHaveBeenCalledWith('[offscreen]', 'arg1', 'arg2'); + }, + ); + + it('uses source from message for prefix', () => { + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'kernel-worker', + method: 'log', + args: ['test'], + }, + }; + + handleConsoleForwardMessage(message); + + expect(consoleSpy).toHaveBeenCalledWith('[kernel-worker]', 'test'); + }); + + it('handles vat source prefixes', () => { + consoleSpy = vi + .spyOn(console, 'info') + .mockImplementation(() => undefined); + + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'vat-v1', + method: 'info', + args: ['vat message'], + }, + }; + + handleConsoleForwardMessage(message); + + expect(consoleSpy).toHaveBeenCalledWith('[vat-v1]', 'vat message'); + }); + }); + + describe('setupPostMessageConsoleForwarding', () => { + let originalConsole: typeof console; + let mockPostMessage: ReturnType; + + beforeEach(() => { + originalConsole = { ...console }; + mockPostMessage = vi.fn(); + vi.stubGlobal('window', { + parent: { + postMessage: mockPostMessage, + }, + }); + }); + + afterEach(() => { + // Restore original console methods + Object.assign(console, originalConsole); + vi.unstubAllGlobals(); + // Re-stub harden since unstubAllGlobals removes it + vi.stubGlobal( + 'harden', + vi.fn((obj: unknown) => obj), + ); + }); + + it('wraps all console methods', () => { + setupPostMessageConsoleForwarding('vat-v1'); + + const methods = ['log', 'debug', 'info', 'warn', 'error'] as const; + for (const method of methods) { + expect(console[method]).not.toBe(originalConsole[method]); + } + }); + + it.each(['log', 'debug', 'info', 'warn', 'error'] as const)( + 'posts %s method to parent window with standard message format', + (method) => { + setupPostMessageConsoleForwarding('vat-v1'); + + console[method]('test message', 123); + + expect(mockPostMessage).toHaveBeenCalledWith( + { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source: 'vat-v1', + method, + args: ['test message', '123'], + }, + }, + '*', + ); + }, + ); + + it('sends messages that pass isConsoleForwardMessage check', () => { + setupPostMessageConsoleForwarding('vat-v1'); + + console.log('test'); + + const sentMessage = mockPostMessage.mock.calls[0][0]; + expect(isConsoleForwardMessage(sentMessage)).toBe(true); + }); + + it('calls original console method', () => { + // Spy on console.log BEFORE setupPostMessageConsoleForwarding captures it + const originalLog = vi.spyOn(console, 'log'); + setupPostMessageConsoleForwarding('vat-v1'); + + console.log('test'); + + expect(originalLog).toHaveBeenCalledWith('test'); + originalLog.mockRestore(); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/src/utils/console-forwarding.ts b/packages/kernel-browser-runtime/src/utils/console-forwarding.ts new file mode 100644 index 000000000..e2909fdbb --- /dev/null +++ b/packages/kernel-browser-runtime/src/utils/console-forwarding.ts @@ -0,0 +1,139 @@ +import type { JsonRpcMessage } from '@metamask/kernel-utils'; +import type { DuplexStream } from '@metamask/streams'; +import type { JsonRpcNotification } from '@metamask/utils'; + +/** + * Message type for forwarding console output from one context to another. + * Used to capture console logs from offscreen documents in Playwright tests. + */ +export type ConsoleForwardMessage = JsonRpcNotification & { + method: 'console-forward'; + params: { + source: string; + method: 'log' | 'debug' | 'info' | 'warn' | 'error'; + args: string[]; + }; +}; + +/** + * Type guard for console-forward messages. + * + * @param value - The value to check. + * @returns Whether the value is a ConsoleForwardMessage. + */ +export const isConsoleForwardMessage = ( + value: unknown, +): value is ConsoleForwardMessage => + typeof value === 'object' && + value !== null && + 'method' in value && + (value as { method: unknown }).method === 'console-forward'; + +/** + * Stringifies an argument for console forwarding. + * + * @param arg - The argument to stringify. + * @returns The stringified argument. + */ +export function stringifyConsoleArg(arg: unknown): string { + if (typeof arg === 'string') { + return arg; + } + if (typeof arg === 'number' || typeof arg === 'boolean') { + return String(arg); + } + // Objects, arrays, null, undefined, functions, symbols, etc. + return JSON.stringify(arg); +} + +/** + * Wraps console methods to forward messages to background via a stream. + * This enables capturing console output from contexts that Playwright cannot + * directly access (like offscreen documents). + * + * Call this early after the stream is created. After setup, console output + * will be forwarded to the stream recipient where it can be replayed. + * + * @param stream - The stream to write console messages to. + * @param source - The source identifier for this context (e.g., 'offscreen', 'kernel-worker'). + */ +export function setupConsoleForwarding( + stream: DuplexStream, + source: string, +): void { + const originalConsole = { ...console }; + const consoleMethods = ['log', 'debug', 'info', 'warn', 'error'] as const; + + consoleMethods.forEach((consoleMethod) => { + // eslint-disable-next-line no-console + console[consoleMethod] = (...args: unknown[]) => { + // Call original console method + originalConsole[consoleMethod](...args); + + // Forward to background via stream + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source, + method: consoleMethod, + args: args.map(stringifyConsoleArg), + }, + }; + stream.write(message).catch(() => { + // Ignore errors if stream isn't ready + }); + }; + }); + + harden(globalThis.console); +} + +/** + * Handles a console-forward message by replaying it to the local console. + * Use this in the stream handler to replay forwarded console output. + * + * @param message - The console-forward message to handle. + */ +export function handleConsoleForwardMessage( + message: ConsoleForwardMessage, +): void { + const { source, method, args } = message.params; + // eslint-disable-next-line no-console + console[method](`[${source}]`, ...args); +} + +/** + * Wraps console methods to forward messages to parent window via postMessage. + * Use this in iframes that don't have a direct stream connection to background. + * + * Messages are sent in the standard ConsoleForwardMessage format so they can + * be validated with isConsoleForwardMessage on the receiving end. + * + * @param source - The source identifier for this context (e.g., 'vat-v1'). + */ +export function setupPostMessageConsoleForwarding(source: string): void { + const originalConsole = { ...console }; + const consoleMethods = ['log', 'debug', 'info', 'warn', 'error'] as const; + + consoleMethods.forEach((consoleMethod) => { + // eslint-disable-next-line no-console + console[consoleMethod] = (...args: unknown[]) => { + originalConsole[consoleMethod](...args); + + // Post to parent window using standard ConsoleForwardMessage format + const message: ConsoleForwardMessage = { + jsonrpc: '2.0', + method: 'console-forward', + params: { + source, + method: consoleMethod, + args: args.map(stringifyConsoleArg), + }, + }; + window.parent.postMessage(message, '*'); + }; + }); + + harden(globalThis.console); +} diff --git a/packages/kernel-browser-runtime/src/utils/index.test.ts b/packages/kernel-browser-runtime/src/utils/index.test.ts index 1defa9bf6..5110d3345 100644 --- a/packages/kernel-browser-runtime/src/utils/index.test.ts +++ b/packages/kernel-browser-runtime/src/utils/index.test.ts @@ -7,7 +7,12 @@ describe('index', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ 'createRelayQueryString', 'getRelaysFromCurrentLocation', + 'handleConsoleForwardMessage', + 'isConsoleForwardMessage', 'parseRelayQueryString', + 'setupConsoleForwarding', + 'setupPostMessageConsoleForwarding', + 'stringifyConsoleArg', ]); }); }); diff --git a/packages/kernel-browser-runtime/src/utils/index.ts b/packages/kernel-browser-runtime/src/utils/index.ts index c77e8b124..4e189403c 100644 --- a/packages/kernel-browser-runtime/src/utils/index.ts +++ b/packages/kernel-browser-runtime/src/utils/index.ts @@ -1 +1,2 @@ +export * from './console-forwarding.ts'; export * from './relay-query-string.ts'; diff --git a/packages/kernel-browser-runtime/src/vat/iframe.ts b/packages/kernel-browser-runtime/src/vat/iframe.ts index 2e914fa6a..f4333d4f1 100644 --- a/packages/kernel-browser-runtime/src/vat/iframe.ts +++ b/packages/kernel-browser-runtime/src/vat/iframe.ts @@ -8,6 +8,8 @@ import { } from '@metamask/streams/browser'; import { makePlatform } from '@ocap/kernel-platforms/browser'; +import { setupPostMessageConsoleForwarding } from '../utils/console-forwarding.ts'; + const logger = new Logger('vat-iframe'); main().catch(logger.error); @@ -29,6 +31,9 @@ async function main(): Promise { const urlParams = new URLSearchParams(window.location.search); const vatId = urlParams.get('vatId') ?? 'unknown'; + // Set up console forwarding to parent (offscreen) for Playwright capture + setupPostMessageConsoleForwarding(`vat-${vatId}`); + // eslint-disable-next-line no-new new VatSupervisor({ id: vatId, diff --git a/packages/kernel-errors/package.json b/packages/kernel-errors/package.json index 607715278..d2d76ee28 100644 --- a/packages/kernel-errors/package.json +++ b/packages/kernel-errors/package.json @@ -42,7 +42,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-errors", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-errors", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-language-model-service/package.json b/packages/kernel-language-model-service/package.json index 68b51265e..77e79a90d 100644 --- a/packages/kernel-language-model-service/package.json +++ b/packages/kernel-language-model-service/package.json @@ -52,7 +52,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel-language-model-service", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-platforms/package.json b/packages/kernel-platforms/package.json index 277f8b671..72b9e66a0 100644 --- a/packages/kernel-platforms/package.json +++ b/packages/kernel-platforms/package.json @@ -52,7 +52,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel-platforms", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-rpc-methods/package.json b/packages/kernel-rpc-methods/package.json index 06f145ac5..ec4c596c3 100644 --- a/packages/kernel-rpc-methods/package.json +++ b/packages/kernel-rpc-methods/package.json @@ -42,7 +42,7 @@ "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-rpc-methods", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-rpc-methods", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-shims/package.json b/packages/kernel-shims/package.json index e11f3c1c7..cb366270d 100644 --- a/packages/kernel-shims/package.json +++ b/packages/kernel-shims/package.json @@ -35,7 +35,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-shims", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-shims", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-store/package.json b/packages/kernel-store/package.json index 743a9dffc..1bdf824a3 100644 --- a/packages/kernel-store/package.json +++ b/packages/kernel-store/package.json @@ -63,7 +63,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-store", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-store", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-test-local/package.json b/packages/kernel-test-local/package.json index 45438e657..4dbce2c09 100644 --- a/packages/kernel-test-local/package.json +++ b/packages/kernel-test-local/package.json @@ -13,7 +13,7 @@ }, "type": "module", "scripts": { - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index b5f09cbbd..6b73472b0 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -31,7 +31,7 @@ ], "scripts": { "build": "ocap bundle src/vats", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo './src/**/*.bundle'", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo './src/**/*.bundle' ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-ui/package.json b/packages/kernel-ui/package.json index a39668e32..9e34ed5fe 100644 --- a/packages/kernel-ui/package.json +++ b/packages/kernel-ui/package.json @@ -45,7 +45,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-ui", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-ui", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json index 0c269fb5c..4754ef085 100644 --- a/packages/kernel-utils/package.json +++ b/packages/kernel-utils/package.json @@ -62,7 +62,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/kernel-utils", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/kernel-utils", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/logger/package.json b/packages/logger/package.json index 7e5358973..43a32dfd5 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -42,7 +42,7 @@ "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/logger", "changelog:update": "../../scripts/update-changelog.sh @metamask/logger", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/nodejs-test-workers/package.json b/packages/nodejs-test-workers/package.json index 27752f6a1..1a4a93fbc 100644 --- a/packages/nodejs-test-workers/package.json +++ b/packages/nodejs-test-workers/package.json @@ -32,7 +32,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/nodejs-test-workers", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index f314bd381..9409e8d83 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -32,7 +32,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/nodejs", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json index 0e12041ae..0c4b13e4a 100644 --- a/packages/ocap-kernel/package.json +++ b/packages/ocap-kernel/package.json @@ -53,7 +53,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/ocap-kernel", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/ocap-kernel", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/omnium-gatherum/package.json b/packages/omnium-gatherum/package.json index cf8fd859b..36bf371b4 100644 --- a/packages/omnium-gatherum/package.json +++ b/packages/omnium-gatherum/package.json @@ -23,7 +23,7 @@ "build:caplets": "ocap bundle src/caplets/echo", "build:vite": "vite build --configLoader runner --config vite.config.ts", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/omnium-gatherum", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", @@ -70,7 +70,7 @@ "@metamask/eslint-config-typescript": "^15.0.0", "@ocap/cli": "workspace:^", "@ocap/repo-tools": "workspace:^", - "@playwright/test": "^1.55.1", + "@playwright/test": "^1.57.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -94,7 +94,7 @@ "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-promise": "^7.2.1", "jsdom": "^27.4.0", - "playwright": "^1.55.1", + "playwright": "^1.57.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", "tsx": "^4.20.6", diff --git a/packages/remote-iterables/package.json b/packages/remote-iterables/package.json index 0b565c7fb..d94032059 100644 --- a/packages/remote-iterables/package.json +++ b/packages/remote-iterables/package.json @@ -32,7 +32,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/remote-iterables", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 7022ea338..ac802a298 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -29,7 +29,7 @@ "dist/" ], "scripts": { - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", @@ -47,7 +47,7 @@ "@metamask/eslint-config-nodejs": "^15.0.0", "@metamask/eslint-config-typescript": "^15.0.0", "@metamask/superstruct": "^3.2.1", - "@playwright/test": "^1.55.1", + "@playwright/test": "^1.57.0", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", "@typescript-eslint/utils": "^8.29.0", diff --git a/packages/repo-tools/src/test-utils/extension.ts b/packages/repo-tools/src/test-utils/extension.ts index 065107f52..6fbdbecec 100644 --- a/packages/repo-tools/src/test-utils/extension.ts +++ b/packages/repo-tools/src/test-utils/extension.ts @@ -1,11 +1,46 @@ -import { chromium } from '@playwright/test'; -import type { BrowserContext, Page } from '@playwright/test'; -import { rm } from 'node:fs/promises'; +import { chromium, test } from '@playwright/test'; +import type { + BrowserContext, + CDPSession, + ConsoleMessage, + Page, +} from '@playwright/test'; +import { appendFileSync } from 'node:fs'; +import { mkdir, rm, readFile, access } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +// CDP event types for Runtime domain +// These are simplified versions of the Chrome DevTools Protocol types +type CdpRemoteObject = { + type: string; + value?: unknown; + description?: string; +}; + +type CdpExecutionContextCreatedEvent = { + context: { + id: number; + origin: string; + auxData?: { frameId?: string }; + }; +}; + +type CdpConsoleAPICalledEvent = { + type: string; + args: CdpRemoteObject[]; + executionContextId: number; +}; + export const sessionPath = path.resolve(os.tmpdir(), 'ocap-test'); +// Run ID is generated once per Playwright invocation (per worker process) +// This allows associating all test log files from the same run +const runId = new Date() + .toISOString() + .slice(0, -5) // Remove ".123Z" + .replace(/[:.]/gu, '-'); // Make filename-safe + type Options = { contextId?: string | undefined; extensionPath: string; @@ -21,7 +56,7 @@ type Options = { * @param options.extensionPath - The path to the extension dist folder. * @param options.onPageLoad - Optional callback to run after the extension is loaded. Useful for * e.g. waiting for components to be visible before proceeding with a test. - * @returns The extension context, extension ID, and popup page + * @returns The extension context, extension ID, popup page, log file path, and cleanup function */ export const makeLoadExtension = async ({ contextId, @@ -31,6 +66,8 @@ export const makeLoadExtension = async ({ browserContext: BrowserContext; extensionId: string; popupPage: Page; + logFilePath: string; + attachLogs: () => Promise; }> => { const workerIndex = process.env.TEST_WORKER_INDEX ?? '0'; // Use provided contextId or fall back to workerIndex for separate user data dirs @@ -38,6 +75,60 @@ export const makeLoadExtension = async ({ const userDataDir = path.join(sessionPath, effectiveContextId); await rm(userDataDir, { recursive: true, force: true }); + // Set up log file for capturing console output from extension contexts + const packageRoot = path.dirname(extensionPath); // extensionPath is /dist + const logsDir = path.join(packageRoot, 'logs'); + await mkdir(logsDir, { recursive: true }); + const testTitle = test + .info() + .titlePath.join('-') + .replace(/[^a-zA-Z0-9-]/gu, '_'); // Make filename-safe + const logFilePath = path.join(logsDir, `${runId}-${testTitle}.log`); + + /** + * Attaches the log file to test results. Call this at the end of your test + * to include console logs in the Playwright HTML report. + */ + const attachLogs = async (): Promise => { + try { + await access(logFilePath); + const content = await readFile(logFilePath, 'utf-8'); + await test.info().attach('console-logs', { + body: content, + contentType: 'text/plain', + }); + } catch { + // File doesn't exist, nothing to attach + } + }; + + const writeLog = (source: string, consoleMessage: ConsoleMessage): void => { + const logTimestamp = new Date().toISOString().slice(0, -5); + const text = consoleMessage.text(); + const type = consoleMessage.type(); + // eslint-disable-next-line n/no-sync + appendFileSync( + logFilePath, + `[${logTimestamp}] [${source}] [${type}] ${text}\n`, + ); + }; + + /** + * Write a raw log entry (for CDP events where we don't have a ConsoleMessage). + * + * @param source - The source identifier for the log. + * @param type - The console method type. + * @param text - The log message text. + */ + const writeRawLog = (source: string, type: string, text: string): void => { + const logTimestamp = new Date().toISOString().slice(0, -5); + // eslint-disable-next-line n/no-sync + appendFileSync( + logFilePath, + `[${logTimestamp}] [${source}] [${type}] ${text}\n`, + ); + }; + const browserArgs = [ `--disable-features=ExtensionDisableUnsupportedDeveloper`, `--disable-extensions-except=${extensionPath}`, @@ -56,6 +147,40 @@ export const makeLoadExtension = async ({ args: browserArgs, }); + // Capture background service worker console logs + browserContext.on('serviceworker', (worker) => { + worker.on('console', (consoleMessage) => + writeLog('background', consoleMessage), + ); + }); + + // Track CDP sessions for cleanup + const cdpSessions: CDPSession[] = []; + + // Capture console logs from extension pages (offscreen document, etc.) + // Note: Pages may start at about:blank, so we attach the listener and check URL in the handler + browserContext.on('page', (page) => { + page.on('console', (consoleMessage) => { + if (page.url().includes('offscreen.html')) { + writeLog('offscreen', consoleMessage); + } + }); + + // Capture Web Worker console logs (e.g., kernel worker) + page.on('worker', (worker) => { + worker.on('console', (consoleMessage) => { + writeLog('kernel-worker', consoleMessage); + }); + }); + + // Set up CDP to capture iframe console logs (vat iframes) + // We need to do this because Playwright doesn't have frame.on('console') + setupCdpForIframeConsoleLogs(page, writeRawLog, cdpSessions).catch( + // eslint-disable-next-line no-console + (error) => console.warn('Failed to set up CDP for iframe logs:', error), + ); + }); + // Wait for the extension to be loaded await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -70,8 +195,84 @@ export const makeLoadExtension = async ({ } const popupPage = await browserContext.newPage(); + popupPage.on('console', (consoleMessage) => + writeLog('popup', consoleMessage), + ); await popupPage.goto(`chrome-extension://${extensionId}/popup.html`); await onPageLoad(popupPage); - return { browserContext, extensionId, popupPage }; + return { browserContext, extensionId, popupPage, logFilePath, attachLogs }; }; + +/** + * Sets up Chrome DevTools Protocol (CDP) to capture console logs from iframes. + * Playwright doesn't provide `frame.on('console')`, so we use CDP's Runtime domain + * to listen for console API calls from all execution contexts including iframes. + * + * @param page - The Playwright page to set up CDP for. + * @param writeRawLog - Function to write raw log entries. + * @param cdpSessions - Array to track CDP sessions for cleanup. + */ +async function setupCdpForIframeConsoleLogs( + page: Page, + writeRawLog: (source: string, type: string, text: string) => void, + cdpSessions: CDPSession[], +): Promise { + // Only set up CDP for pages that might have iframes (offscreen document) + if (!page.url().includes('offscreen.html')) { + return; + } + + const cdpSession = await page.context().newCDPSession(page); + cdpSessions.push(cdpSession); + + // Enable Runtime domain to receive console events + await cdpSession.send('Runtime.enable'); + + // Track execution contexts to identify iframe sources + const executionContexts = new Map(); + + // Listen for new execution contexts (iframes get their own context) + cdpSession.on( + 'Runtime.executionContextCreated', + (event: CdpExecutionContextCreatedEvent) => { + const { id, origin, auxData } = event.context; + // auxData.frameId can help identify the iframe + const frameId = auxData?.frameId; + const source = frameId ? `iframe-${frameId.slice(0, 8)}` : `ctx-${id}`; + executionContexts.set(id, origin.includes('iframe') ? source : origin); + }, + ); + + // Listen for console API calls from all contexts (including iframes) + cdpSession.on( + 'Runtime.consoleAPICalled', + (event: CdpConsoleAPICalledEvent) => { + const { type, args, executionContextId } = event; + + // Format args into a readable string + const text = args + .map((arg) => { + if (arg.value !== undefined) { + return typeof arg.value === 'string' + ? arg.value + : JSON.stringify(arg.value); + } + return arg.description ?? arg.type; + }) + .join(' '); + + // Determine the source based on execution context + const contextSource = executionContexts.get(executionContextId); + const source = contextSource?.startsWith('iframe') + ? contextSource + : `iframe-ctx-${executionContextId}`; + + // Only log if it looks like an iframe context (avoid duplicating main page logs) + // Main page logs are already captured via page.on('console') + if (contextSource?.startsWith('iframe') || executionContextId > 1) { + writeRawLog(source, type, text); + } + }, + ); +} diff --git a/packages/streams/package.json b/packages/streams/package.json index 0cfd55100..a640802b5 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -53,7 +53,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/streams", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/streams", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", @@ -102,7 +102,7 @@ "eslint-plugin-n": "^17.17.0", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-promise": "^7.2.1", - "playwright": "^1.55.1", + "playwright": "^1.57.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", "ses": "^1.14.0", diff --git a/packages/template-package/package.json b/packages/template-package/package.json index be5ffa909..b3dd07130 100644 --- a/packages/template-package/package.json +++ b/packages/template-package/package.json @@ -32,7 +32,7 @@ "build": "ts-bridge --project tsconfig.build.json --no-references --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/template-package", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/yarn.config.cjs b/yarn.config.cjs index 872cb184d..2f1a4055e 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -204,8 +204,12 @@ module.exports = defineConfig({ expectWorkspaceField(workspace, 'scripts.build:docs', 'typedoc'); } - // All packages except the root must have a "clean" script. - expectWorkspaceField(workspace, 'scripts.clean'); + // All packages except the root must have a "clean" script that includes ./logs. + expectWorkspaceField(workspace, 'scripts.clean', (currentValue) => + typeof currentValue === 'string' && !currentValue.includes('./logs') + ? `${currentValue} ./logs` + : currentValue, + ); // No non-root packages may have a "prepack" script. workspace.unset('scripts.prepack'); diff --git a/yarn.lock b/yarn.lock index 084a341ef..c5cbd044b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2931,7 +2931,7 @@ __metadata: eslint-plugin-n: "npm:^17.17.0" eslint-plugin-prettier: "npm:^5.2.6" eslint-plugin-promise: "npm:^7.2.1" - playwright: "npm:^1.55.1" + playwright: "npm:^1.57.0" prettier: "npm:^3.5.3" rimraf: "npm:^6.0.1" ses: "npm:^1.14.0" @@ -3477,7 +3477,7 @@ __metadata: "@ocap/cli": "workspace:^" "@ocap/kernel-test": "workspace:^" "@ocap/repo-tools": "workspace:^" - "@playwright/test": "npm:^1.55.1" + "@playwright/test": "npm:^1.57.0" "@testing-library/jest-dom": "npm:^6.6.3" "@types/chrome": "npm:^0.0.313" "@types/react": "npm:^18.3.18" @@ -3497,7 +3497,7 @@ __metadata: eslint-plugin-prettier: "npm:^5.2.6" eslint-plugin-promise: "npm:^7.2.1" jsdom: "npm:^27.4.0" - playwright: "npm:^1.55.1" + playwright: "npm:^1.57.0" prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" @@ -3942,7 +3942,7 @@ __metadata: "@metamask/utils": "npm:^11.9.0" "@ocap/cli": "workspace:^" "@ocap/repo-tools": "workspace:^" - "@playwright/test": "npm:^1.55.1" + "@playwright/test": "npm:^1.57.0" "@testing-library/dom": "npm:^10.4.0" "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/react": "npm:^16.3.0" @@ -3967,7 +3967,7 @@ __metadata: eslint-plugin-promise: "npm:^7.2.1" immer: "npm:^10.1.1" jsdom: "npm:^27.4.0" - playwright: "npm:^1.55.1" + playwright: "npm:^1.57.0" prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" @@ -4035,7 +4035,7 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^15.0.0" "@metamask/eslint-config-typescript": "npm:^15.0.0" "@metamask/superstruct": "npm:^3.2.1" - "@playwright/test": "npm:^1.55.1" + "@playwright/test": "npm:^1.57.0" "@typescript-eslint/eslint-plugin": "npm:^8.29.0" "@typescript-eslint/parser": "npm:^8.29.0" "@typescript-eslint/utils": "npm:^8.29.0" @@ -4556,14 +4556,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.55.1": - version: 1.58.0 - resolution: "@playwright/test@npm:1.58.0" +"@playwright/test@npm:^1.57.0": + version: 1.57.0 + resolution: "@playwright/test@npm:1.57.0" dependencies: - playwright: "npm:1.58.0" + playwright: "npm:1.57.0" bin: playwright: cli.js - checksum: 10/1ab8c4d408c919e1357bb43e5682d1ce66bb792516fd6269dd5cff9c9b7cc82e026b3fc864ba723714337dc610ac46110ed8307d610feb92f5002abbb03a6392 + checksum: 10/07f5ba4841b2db1dea70d821004c5156b692488e13523c096ce3487d30f95f34ccf30ba6467ece60c86faac27ae382213b7eacab48a695550981b2e811e5e579 languageName: node linkType: hard @@ -12484,27 +12484,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.58.0": - version: 1.58.0 - resolution: "playwright-core@npm:1.58.0" +"playwright-core@npm:1.57.0": + version: 1.57.0 + resolution: "playwright-core@npm:1.57.0" bin: playwright-core: cli.js - checksum: 10/718549cdcd22e55f42887e7b832276c9a50e112b278d22a2242bb00c401021623fedf9554c7090f132e389b5bbe11a987ce71e3c877e767eed4dd9bfe573019c + checksum: 10/ec066602f0196f036006caee14a30d0a57533a76673bb9a0c609ef56e21decf018f0e8d402ba2fb18251393be6a1c9e193c83266f1670fe50838c5340e220de0 languageName: node linkType: hard -"playwright@npm:1.58.0, playwright@npm:^1.55.1": - version: 1.58.0 - resolution: "playwright@npm:1.58.0" +"playwright@npm:1.57.0, playwright@npm:^1.57.0": + version: 1.57.0 + resolution: "playwright@npm:1.57.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.58.0" + playwright-core: "npm:1.57.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10/5ef5d0977906046400cee9a359f18c87eafb3e3193a33a22351cc84740c0dffb3cff7b9f2320d1e200817dbf24e63b747a0546b6c671a9fd95937e2402682ad9 + checksum: 10/241559210f98ef11b6bd6413f2d29da7ef67c7865b72053192f0d164fab9e0d3bd47913b3351d5de6433a8aff2d8424d4b8bd668df420bf4dda7ae9fcd37b942 languageName: node linkType: hard