src/__tests__/e2e/fixtures/index.ts
File Relationships
Architecture violations
- [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 };