src/__tests__/e2e/fixtures/index.ts

Total Symbols
3
Lines of Code
201
Avg Complexity
7.0
Symbol Types
2

File Relationships

graph LR getChromiumExecutablePath["getChromiumExecutablePath"] collectAndSaveCoverage["collectAndSaveCoverage"] getChromiumExecutablePath -->|calls| getChromiumExecutablePath getChromiumExecutablePath -->|calls| collectAndSaveCoverage click getChromiumExecutablePath "../symbols/cf77c1808ae2925d.html" click collectAndSaveCoverage "../symbols/ade0cf5b946e9936.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'collectAndSaveCoverage' has cyclomatic complexity 17 (max 10)
  • [warning] max-lines: 'collectAndSaveCoverage' has 84 lines (max 80)

Symbols by Kind

function 2
interface 1

All Symbols

Name Kind Visibility Status Lines Signature
getChromiumExecutablePath function - 19-27 getChromiumExecutablePath(): : string
collectAndSaveCoverage function - 29-112 collectAndSaveCoverage( page: Page, background: Worker, context: BrowserContext, testInfo: TestInfo, ): : Promise<void>
ExtensionFixtures interface - 114-129 interface ExtensionFixtures

Full Source

/// <reference types="node" />
import { test as base, chromium, expect } from "@playwright/test";
import type { BrowserContext, Page, Worker, TestInfo } from "@playwright/test";
import { createCoverageMap } from "istanbul-lib-coverage";
import type { CoverageMapData } from "istanbul-lib-coverage";
import fs from "fs";
import path from "path";

const DIST_PATH = path.join(process.cwd(), "dist");
const COVERAGE_OUTPUT = path.join(process.cwd(), ".coverage", "e2e");

/**
 * Returns the Chrome for Testing executable path (bundled by Playwright) for
 * use with `launchPersistentContext`. Regular stable Chrome ignores
 * `--load-extension` unless Developer Mode is already enabled in the profile,
 * so we always prefer Chrome for Testing (which has no such restriction).
 * The `CHROME_PATH` env var can be used to override for exotic CI setups.
 */
function getChromiumExecutablePath(): string {
  if (process.env.CHROME_PATH) return process.env.CHROME_PATH;
  // chromium.executablePath() resolves the Playwright-bundled Chrome for Testing
  try {
    return chromium.executablePath();
  } catch {
    return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
  }
}

async function collectAndSaveCoverage(
  page: Page,
  background: Worker,
  context: BrowserContext,
  testInfo: TestInfo,
): Promise<void> {
  const map = createCoverageMap({});

  // 1. Service worker (background) — globalThis.__coverage__
  try {
    const swCov = await background.evaluate<CoverageMapData>(() =>
      JSON.parse(
        JSON.stringify(
          (globalThis as unknown as Record<string, unknown>)["__coverage__"] ??
            {},
        ),
      ),
    );
    if (Object.keys(swCov).length) map.merge(swCov);
  } catch {
    // SW may not have coverage data when VITE_COVERAGE is not set
  }

  // 2. Content scripts (isolated world) — collected via background scripting API
  try {
    const raw = await background.evaluate(async () => {
      const tabs = await chrome.tabs.query({ url: "http://localhost/*" });
      const results: string[] = [];
      for (const tab of tabs) {
        if (!tab.id) continue;
        try {
          const [res] = await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            world: "ISOLATED",
            func: (): string =>
              JSON.stringify(
                (window as unknown as Record<string, unknown>)[
                  "__coverage__"
                ] ?? {},
              ),
          });
          if (res?.result) results.push(res.result as string);
        } catch {
          // Tab may have been closed or scripting not permitted
        }
      }
      return results;
    });
    for (const serialized of raw) {
      const cov = JSON.parse(serialized) as CoverageMapData;
      if (Object.keys(cov).length) map.merge(cov);
    }
  } catch {
    // Background scripting API unavailable
  }

  // 3. Extension pages (popup, options, devtools) — window.__coverage__
  for (const pg of context.pages()) {
    if (pg === page) continue;
    try {
      const pageCov = await pg.evaluate<CoverageMapData>(() =>
        JSON.parse(
          JSON.stringify(
            (window as unknown as Record<string, unknown>)["__coverage__"] ??
              {},
          ),
        ),
      );
      if (Object.keys(pageCov).length) map.merge(pageCov);
    } catch {
      // Page may have been closed
    }
  }

  fs.mkdirSync(COVERAGE_OUTPUT, { recursive: true });
  const safeName = testInfo.titlePath
    .join("_")
    .replace(/[^a-z0-9_]/gi, "-")
    .slice(0, 100);
  fs.writeFileSync(
    path.join(COVERAGE_OUTPUT, `${safeName}.json`),
    JSON.stringify(map.toJSON(), null, 2),
  );
}

interface ExtensionFixtures {
  /**
   * Override built-in `context` with a persistent Chrome context that has
   * the Fill All extension loaded. MV3 service workers require a persistent
   * user data directory — regular browser contexts do NOT support them.
   */
  context: BrowserContext;
  /** Page created from the extension-enabled persistent context. */
  page: Page;
  /** The extension background service worker. */
  background: Worker;
  /** The extension ID extracted from the service worker URL. */
  extensionId: string;
  /** Auto fixture — collects JS coverage from chrome-extension:// URLs. */
  _coverage: void;
}

/**
 * Extended Playwright `test` that:
 * 1. Loads the Fill All extension via `launchPersistentContext` (required for MV3)
 * 2. Overrides `context` and `page` so all tests use the extension-enabled context
 * 3. Provides `background` (service worker) and `extensionId`
 * 4. Auto-collects JS coverage from chrome-extension:// URLs
 *
 * Usage:
 * ```ts
 * import { test, expect } from "@/__tests__/e2e/fixtures";
 *
 * test("fill form", async ({ page }) => {
 *   await page.goto("/test-form.html");
 *   // extension is already loaded — content script will be injected
 * });
 * ```
 */
export const test = base.extend<ExtensionFixtures>({
  // Override built-in `context` — creates a new Chrome process with the
  // extension loaded for every test (scope: "test" for full isolation).
  context: [
    async ({}, use) => {
      const context = await chromium.launchPersistentContext("", {
        headless: false,
        executablePath: getChromiumExecutablePath(),
        args: [
          `--disable-extensions-except=${DIST_PATH}`,
          `--load-extension=${DIST_PATH}`,
          "--no-first-run",
          "--no-default-browser-check",
          "--disable-infobars",
          "--disable-popup-blocking",
        ],
      });
      await use(context);
      await context.close();
    },
    { scope: "test" },
  ],

  // Override built-in `page` — creates a page from the persistent context.
  page: async ({ context }, use) => {
    const page = await context.newPage();
    await use(page);
    // context.close() in the context fixture will close the page as well.
  },

  // Waits for the extension background service worker to register.
  background: async ({ context }, use) => {
    let [sw] = context.serviceWorkers();
    if (!sw) {
      sw = await context.waitForEvent("serviceworker", { timeout: 15_000 });
    }
    await use(sw);
  },

  extensionId: async ({ background }, use) => {
    await use(background.url().split("/")[2]);
  },

  _coverage: [
    async ({ page, background, context }, use, testInfo) => {
      await use();
      await collectAndSaveCoverage(page, background, context, testInfo);
    },
    { auto: true },
  ],
});

export { expect };