src/__tests__/e2e/fixtures/messaging.ts

Total Symbols
3
Lines of Code
63
Avg Complexity
2.0
Symbol Types
2

Symbols by Kind

function 2
type 1

All Symbols

Name Kind Visibility Status Lines Signature
ExtensionMsg type - 10-10 type ExtensionMsg
sendToContentScript function exported- 18-41 sendToContentScript( page: Page, message: ExtensionMsg, ): : Promise<unknown>
waitForContentScript function exported- 54-62 waitForContentScript(page: Page): : Promise<void>

Full Source

/**
 * E2E helpers for sending Chrome extension messages from Playwright tests.
 *
 * Because content scripts run in an isolated world, the only way to trigger
 * extension functionality from Playwright is via the service worker, which
 * can call `chrome.tabs.sendMessage(tabId, message)`.
 */
import type { Page } from "@playwright/test";

type ExtensionMsg = { type: string; payload?: unknown };

/**
 * Sends a message to the content script running on `page` by going through
 * the background service worker.
 *
 * @returns The response from the content script handler.
 */
export async function sendToContentScript(
  page: Page,
  message: ExtensionMsg,
): Promise<unknown> {
  const context = page.context();

  let [sw] = context.serviceWorkers();
  if (!sw) {
    sw = await context.waitForEvent("serviceworker", { timeout: 10_000 });
  }

  const url = page.url();

  return sw.evaluate(
    async ({ pageUrl, msg }) => {
      const origin = new URL(pageUrl).origin;
      const tabs = await chrome.tabs.query({ url: `${origin}/*` });
      const tab = tabs[0];
      if (!tab?.id) throw new Error(`No tab found for origin: ${origin}`);
      return chrome.tabs.sendMessage(tab.id, msg);
    },
    { pageUrl: url, msg: message },
  );
}

/**
 * Waits for the content script to finish initializing on the given page.
 *
 * The content script registers `chrome.runtime.onMessage` synchronously but
 * may call async `initContentScript()` before being fully operational. We add
 * a small delay after `DOMContentLoaded` to let that settle.
 *
 * If the page exposes `window.__fillAllReady` (set by the content script), we
 * wait for that; otherwise we fall back to a 600 ms timeout which is
 * sufficient for `document_idle` injection.
 */
export async function waitForContentScript(page: Page): Promise<void> {
  await page.waitForFunction(
    () =>
      document.readyState === "complete" ||
      document.readyState === "interactive",
  );
  // The content script is injected at `document_idle`; give it time to init.
  await page.waitForTimeout(600);
}