All files / kernel-browser-runtime/src/utils console-forwarding.ts

100% Statements 14/14
100% Branches 0/0
100% Functions 6/6
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97                      4x                                           4x 17x                                             11x 11x   11x   55x   10x     10x           15x     10x       11x                       7x   7x    
import { stringify } from '@metamask/kernel-utils';
import {
  object,
  literal,
  string,
  array,
  enums,
  is,
} from '@metamask/superstruct';
import type { Infer } from '@metamask/superstruct';
 
const ConsoleForwardMessageStruct = object({
  jsonrpc: literal('2.0'),
  method: literal('console-forward'),
  params: object({
    source: string(),
    method: enums(['log', 'debug', 'info', 'warn', 'error']),
    args: array(string()),
  }),
});
 
/**
 * 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 = Infer<typeof ConsoleForwardMessageStruct>;
 
/**
 * 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 => is(value, ConsoleForwardMessageStruct);
 
/**
 * Wraps console methods to forward messages via a provided callback.
 * This enables capturing console output from contexts that Playwright cannot
 * directly access (like offscreen documents, workers, or iframes).
 *
 * Call this early in the context's initialization. After setup, console output
 * will be forwarded to the callback where it can be sent to a stream, posted
 * to a parent window, or handled in any other way.
 *
 * @param options - The options for setting up console forwarding.
 * @param options.source - The source identifier for this context (e.g., 'offscreen', 'kernel-worker', 'vat-v1').
 * @param options.onMessage - Callback invoked with each console message.
 */
export function setupConsoleForwarding({
  source,
  onMessage,
}: {
  source: string;
  onMessage: (message: ConsoleForwardMessage) => void;
}): 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 via callback
      const message: ConsoleForwardMessage = {
        jsonrpc: '2.0',
        method: 'console-forward',
        params: {
          source,
          method: consoleMethod,
          args: args.map((arg) => stringify(arg, 0)),
        },
      };
      onMessage(message);
    };
  });
 
  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);
}