From c19de451413b713b05f78d3fdfa55c34d608490e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 16:23:08 -0700 Subject: [PATCH 1/7] chore(coverage): istanbul instrumentation for the trace viewer and html reporter Instrument the sources under packages/ with a vite plugin behind PWTEST_COVERAGE and record the coverage of the trace viewer and html reporter pages through tracing. The stable test runner is rolled to 1.64.0-alpha-2026-09-18, the first with the tracing coverage, so the html reporter tests need no collector of their own. The trace viewer service worker has its own global and its own counters. The instrumented build serves them from a /coverage route of the worker, gated on the __PW_COVERAGE__ define that the istanbul plugin sets, so a normal build carries neither the route nor a counter reference. The tests fold the worker delta into window.__coverage__ through the page before the page is collected. Only applyPlaywrightAttributes in the snapshot renderer is skipped with an istanbul hint, since its source is stringified into the snapshot where the counters do not exist. The product feature does not follow for now. Only Chromium exposes a service worker execution context, Firefox's juggler has no service worker targets and WebKit's automation session attaches to page and frame targets only, so both would need a browser patch. Worker counters also die with every termination and there is no unload hook to stash them, so a generic collector would be Chromium-only and lossy. --- .gitignore | 1 + eslint.config.mjs | 1 + packages/html-reporter/vite.config.ts | 2 + packages/injected/src/coverageScript.ts | 31 +--------- packages/isomorphic/istanbulCoverage.ts | 30 ++++++++++ packages/isomorphic/trace/snapshotRenderer.ts | 2 + packages/trace-viewer/src/sw/main.ts | 19 ++++++ packages/trace-viewer/src/vite-env.d.ts | 1 + packages/trace-viewer/vite.config.ts | 4 +- tests/config/serviceWorkerCoverage.ts | 47 +++++++++++++++ tests/config/traceViewerFixtures.ts | 21 ++++--- tests/library/playwright.config.ts | 2 + tests/playwright-test/playwright.config.ts | 6 ++ tests/playwright-test/reporter-html.spec.ts | 6 ++ .../stable-test-runner/package-lock.json | 24 ++++---- .../stable-test-runner/package.json | 2 +- utils/build/viteIstanbul.ts | 60 +++++++++++++++++++ 17 files changed, 207 insertions(+), 52 deletions(-) create mode 100644 tests/config/serviceWorkerCoverage.ts create mode 100644 utils/build/viteIstanbul.ts diff --git a/.gitignore b/.gitignore index a1028105e3b80..ad22efd8bbde2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ /test-results/ +/coverage/ /tests/coverage-report .local-browsers/ /.dev_profile* diff --git a/eslint.config.mjs b/eslint.config.mjs index 8eda40f3a5675..d679710ebe26a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,6 +41,7 @@ const ignores = [ "*.js", "**/.cache/", "**/*.d.ts", + "coverage/", "index.d.ts", "node_modules/", "output/", diff --git a/packages/html-reporter/vite.config.ts b/packages/html-reporter/vite.config.ts index 93ec7ddde5153..d4a17f7de4c2f 100644 --- a/packages/html-reporter/vite.config.ts +++ b/packages/html-reporter/vite.config.ts @@ -20,6 +20,7 @@ import { bundle } from './bundle'; import { storyTypes } from './tests/storyTypes'; import packageJSON from './package.json'; import path from 'path'; +import { istanbul } from '../../utils/build/viteIstanbul'; // https://vitejs.dev/config/ export default defineConfig({ @@ -28,6 +29,7 @@ export default defineConfig({ react(), bundle(), storyTypes({ prefix: packageJSON.name, src: path.resolve(__dirname, 'src'), outFile: path.resolve(__dirname, 'tests/stories.d.ts') }), + istanbul(), ], resolve: { alias: { diff --git a/packages/injected/src/coverageScript.ts b/packages/injected/src/coverageScript.ts index 1852ece1602be..be3116f8ee081 100644 --- a/packages/injected/src/coverageScript.ts +++ b/packages/injected/src/coverageScript.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { kCoverageStashError, kCoverageStashPrefix } from '@isomorphic/istanbulCoverage'; +import { kCoverageStashError, kCoverageStashPrefix, takeBranchCounters, takeCounters } from '@isomorphic/istanbulCoverage'; import type { IstanbulCoverage, IstanbulCoverageDelta, IstanbulFileCoverageDelta } from '@isomorphic/istanbulCoverage'; @@ -134,32 +134,3 @@ export function takeCoverageStashes(global: typeof globalThis, sessionId: string } return result; } - -function takeCounters(counters: { [key: string]: number }): { [key: string]: number } | undefined { - let result: { [key: string]: number } | undefined; - for (const key of Object.keys(counters)) { - const count = counters[key]; - if (!count) - continue; - if (!result) - result = {}; - result[key] = count; - counters[key] = 0; - } - return result; -} - -// Branch counters are positional, so a hit branch is reported with the whole array. -function takeBranchCounters(counters: { [key: string]: number[] }): { [key: string]: number[] } | undefined { - let result: { [key: string]: number[] } | undefined; - for (const key of Object.keys(counters)) { - const counts = counters[key]; - if (!counts.some(Boolean)) - continue; - if (!result) - result = {}; - result[key] = counts.slice(); - counts.fill(0); - } - return result; -} diff --git a/packages/isomorphic/istanbulCoverage.ts b/packages/isomorphic/istanbulCoverage.ts index 7c0111c6f4371..8e85b0bb95906 100644 --- a/packages/isomorphic/istanbulCoverage.ts +++ b/packages/isomorphic/istanbulCoverage.ts @@ -99,3 +99,33 @@ export function mergeIstanbulCoverage(into: Map, d } } } + +// Returns the hit counters and resets them, so that the next take is a delta. +export function takeCounters(counters: { [key: string]: number }): { [key: string]: number } | undefined { + let result: { [key: string]: number } | undefined; + for (const key of Object.keys(counters)) { + const count = counters[key]; + if (!count) + continue; + if (!result) + result = {}; + result[key] = count; + counters[key] = 0; + } + return result; +} + +// Branch counters are positional, so a hit branch is reported with the whole array. +export function takeBranchCounters(counters: { [key: string]: number[] }): { [key: string]: number[] } | undefined { + let result: { [key: string]: number[] } | undefined; + for (const key of Object.keys(counters)) { + const counts = counters[key]; + if (!counts.some(Boolean)) + continue; + if (!result) + result = {}; + result[key] = counts.slice(); + counts.fill(0); + } + return result; +} diff --git a/packages/isomorphic/trace/snapshotRenderer.ts b/packages/isomorphic/trace/snapshotRenderer.ts index 63612c79ae3ad..c983e7305aebe 100644 --- a/packages/isomorphic/trace/snapshotRenderer.ts +++ b/packages/isomorphic/trace/snapshotRenderer.ts @@ -320,6 +320,8 @@ function generateNonce(): string { } function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) { + // Stringified into the snapshot, where coverage counters do not exist. + /* istanbul ignore next */ function applyPlaywrightAttributes(blankSnapshotUrl: string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) { // eslint-disable-next-line no-restricted-globals const win = window; diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index e4adaac67e726..80474abd2c7b3 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { takeBranchCounters, takeCounters } from '@isomorphic/istanbulCoverage'; import { SnapshotServer } from '@isomorphic/trace/snapshotServer'; import { TraceLoader } from '@isomorphic/trace/traceLoader'; import { TraceVersionError } from '@isomorphic/trace/traceModernizer'; @@ -21,6 +22,8 @@ import { TraceVersionError } from '@isomorphic/trace/traceModernizer'; import { Progress, splitProgress } from './progress'; import { FetchTraceLoaderBackend, ZipTraceLoaderBackend } from './traceLoaderBackends'; +import type { IstanbulCoverage } from '@isomorphic/istanbulCoverage'; + type Client = { id: string; url: string; @@ -152,6 +155,10 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/ping') return new Response(null, { status: 200 }); + // Coverage of the worker itself, only in a build instrumented with PWTEST_COVERAGE. + if (__PW_COVERAGE__ && relativePath === '/coverage') + return new Response(JSON.stringify(takeCoverage()), { status: 200, headers: { 'Content-Type': 'application/json' } }); + const isNavigation = !!event.resultingClientId; const client = event.clientId ? await self.clients.get(event.clientId) : undefined; @@ -233,6 +240,18 @@ async function doFetch(event: FetchEvent): Promise { return fetch(event.request); } +// Reading resets the counters, so every response is a delta. +function takeCoverage(): IstanbulCoverage { + const coverage: IstanbulCoverage = (self as any).__coverage__ || {}; + const result = JSON.parse(JSON.stringify(coverage)); + for (const file of Object.values(coverage)) { + takeCounters(file.s); + takeCounters(file.f); + takeBranchCounters(file.b); + } + return result; +} + function downloadHeaders(searchParams: URLSearchParams): Headers | undefined { const name = searchParams.get('dn'); const contentType = searchParams.get('dct'); diff --git a/packages/trace-viewer/src/vite-env.d.ts b/packages/trace-viewer/src/vite-env.d.ts index dbb4c627d26fe..dfe2d023f74cb 100644 --- a/packages/trace-viewer/src/vite-env.d.ts +++ b/packages/trace-viewer/src/vite-env.d.ts @@ -1,3 +1,4 @@ /// declare const __APP_VERSION__: string; +declare const __PW_COVERAGE__: boolean; diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index ac30801db686d..6089f7b60456a 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -20,13 +20,15 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { bundle } from './bundle'; +import { istanbul } from '../../utils/build/viteIstanbul'; // https://vitejs.dev/config/ export default defineConfig({ base: '', plugins: [ react(), - bundle() + bundle(), + istanbul(), ], define: { 'process.env': {}, diff --git a/tests/config/serviceWorkerCoverage.ts b/tests/config/serviceWorkerCoverage.ts new file mode 100644 index 0000000000000..6929037d82a22 --- /dev/null +++ b/tests/config/serviceWorkerCoverage.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type PageLike = { evaluate(pageFunction: any): Promise }; + +// The trace viewer service worker keeps its own counters and serves them as a delta +// from a coverage route that exists in instrumented builds only. Folds them into the +// page's counters, from where the tracing coverage reads. +export async function mergeServiceWorkerCoverage(page: PageLike) { + if (!process.env.PWTEST_COVERAGE) + return; + await page.evaluate(async () => { + if (!navigator.serviceWorker?.controller) + return; + const response = await fetch('coverage'); + if (!response.ok) + return; + const taken = await response.json(); + const coverage = (window as any).__coverage__ ??= {}; + for (const [file, data] of Object.entries(taken)) { + const existing = coverage[file]; + if (!existing) { + coverage[file] = data; + continue; + } + for (const key of Object.keys(data.s)) + existing.s[key] += data.s[key]; + for (const key of Object.keys(data.f)) + existing.f[key] += data.f[key]; + for (const key of Object.keys(data.b)) + existing.b[key] = existing.b[key].map((count: number, i: number) => count + data.b[key][i]); + } + }).catch(() => {}); +} diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index d580fcc488921..5f822f01fdd2f 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -18,6 +18,7 @@ import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } f import { step } from './baseTest'; import path from 'path'; import { CommonFixtures, TestChildProcess } from './commonFixtures'; +import { mergeServiceWorkerCoverage } from './serviceWorkerCoverage'; type BaseTestFixtures = CommonFixtures & { context: BrowserContext; @@ -155,7 +156,7 @@ class TraceViewerPage { export const traceViewerFixtures: Fixtures = { showTraceViewer: async ({ playwright, childProcess, browserName, channel }, use, testInfo) => { const browsers: Browser[] = []; - const tracings: any[] = []; + const tracedPages: Page[] = []; await use(async (trace: string | undefined, { host, port, stdin, cwd } = {}) => { // In WSL the browser runs in the guest and reaches the host over mirrored networking, // which mirrors the IPv4 loopback but not the host's IPv6 `::1`. Both `--host localhost` @@ -182,19 +183,23 @@ export const traceViewerFixtures: Fixtures { ] : [ ['html', { open: 'on-failure', title: 'Playwright Library Tests' }] ]; + if (process.env.PWTEST_COVERAGE) + result.push(['coverage', { outputDir: path.join(__dirname, '..', '..', 'coverage', 'trace-viewer') }]); return result; }; diff --git a/tests/playwright-test/playwright.config.ts b/tests/playwright-test/playwright.config.ts index e942895262c69..3ad2e95597e13 100644 --- a/tests/playwright-test/playwright.config.ts +++ b/tests/playwright-test/playwright.config.ts @@ -29,10 +29,16 @@ const reporters = () => { ] : [ ['list'] ]; + if (process.env.PWTEST_COVERAGE) + result.push(['coverage', { outputDir: path.join(__dirname, '..', '..', 'coverage', 'html-reporter') }]); return result; }; export default defineConfig({ timeout: 30000, + use: { + // Coverage of the html reporter and trace viewer pages, built with PWTEST_COVERAGE=1. + trace: process.env.PWTEST_COVERAGE ? { mode: 'on', snapshots: false, screenshots: false, coverage: true } : 'off', + }, forbidOnly: !!process.env.CI, workers: undefined, snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 5bc4740eaa1f7..1b8b55130aa4c 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -20,6 +20,7 @@ import url from 'url'; import * as yazl from 'yazl'; import { test as baseTest, expect as baseExpect, cliEntrypoint, createImage } from './playwright-test-fixtures'; import { iso, utils } from '../../packages/playwright-core/lib/coreBundle'; +import { mergeServiceWorkerCoverage } from '../config/serviceWorkerCoverage'; type HttpServer = utils.HttpServer; @@ -27,6 +28,11 @@ const { msToString } = iso; const { spawnAsync } = utils; const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise }>({ + page: async ({ page }, use) => { + await use(page); + // The trace viewer worker's counters are read through the page, before it is collected. + await mergeServiceWorkerCoverage(page); + }, showReport: async ({ page }, use, testInfo) => { let server: HttpServer | undefined; await use(async (reportFolder?: string) => { diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 1b109a8894922..8c3dc319357c9 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.64.0-alpha-2026-09-14" + "@playwright/test": "^1.64.0-alpha-2026-09-18" } }, "node_modules/@playwright/test": { - "version": "1.64.0-alpha-2026-09-14", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.0-alpha-2026-09-14.tgz", - "integrity": "sha512-jSB1pCsT/j6ZLHnmc4pBs/9LDJrNJvvV4zmzthdufRk0RaijJTZ9dVqk29qAO6s1+I3u2vme8nsHqjmoEK1sTg==", + "version": "1.64.0-alpha-2026-09-18", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.0-alpha-2026-09-18.tgz", + "integrity": "sha512-IorNcynBpmU85uiXy7jmW4z0DmY4gc1xwihjWWEv+nbcuSWxoNs/elc5S3oujq3o4EXi+seRCbBv/bGpr0GbTw==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.64.0-alpha-2026-09-14" + "playwright": "1.64.0-alpha-2026-09-18" }, "bin": { "playwright": "cli.js" @@ -24,12 +24,12 @@ } }, "node_modules/playwright": { - "version": "1.64.0-alpha-2026-09-14", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0-alpha-2026-09-14.tgz", - "integrity": "sha512-CdpbZ03/Iq4HMth9RP2NHBv/gv9exDLkR3eJEDvS2fKdAjZSNYvL2PrJum3wHe8qOSFR8KzWuyKUx3AZ5vZc4w==", + "version": "1.64.0-alpha-2026-09-18", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0-alpha-2026-09-18.tgz", + "integrity": "sha512-90N+GRyQZGvujyIxbSDt+3I0UoOw3M/Uq0cuJlWKsW9teGnc2DhXmNea9mIv78M0VmzHxH4o4dfjA30brJhGdw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.64.0-alpha-2026-09-14" + "playwright-core": "1.64.0-alpha-2026-09-18" }, "bin": { "playwright": "cli.js" @@ -39,9 +39,9 @@ } }, "node_modules/playwright-core": { - "version": "1.64.0-alpha-2026-09-14", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0-alpha-2026-09-14.tgz", - "integrity": "sha512-VCqlQ8lEKS8NAfEU/sWx3KP7gL2oCWd/DAGzPDZncGV7dUNNHpaVxHd2LWe2pc5y53h0Hqu3SWzRiPqKqkrYQA==", + "version": "1.64.0-alpha-2026-09-18", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0-alpha-2026-09-18.tgz", + "integrity": "sha512-7BkKuCYyNBrVGtMKkcnUQqi+/Y8KO4oU395pTeCOqSUKVTjSXE/yXoE7ihLiO/JrU+vljL1hMmw/ArvuWOUPLQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index d02bb1f41e30e..2f0d6d30b1083 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.64.0-alpha-2026-09-14" + "@playwright/test": "^1.64.0-alpha-2026-09-18" } } diff --git a/utils/build/viteIstanbul.ts b/utils/build/viteIstanbul.ts new file mode 100644 index 0000000000000..5d6ab5da2f51f --- /dev/null +++ b/utils/build/viteIstanbul.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Plugin } from 'vite'; + +// Instruments the sources under packages/ for istanbul coverage when PWTEST_COVERAGE is set. +// Requires istanbul-lib-instrument, which is not a dependency of the repo: +// npm i --no-save istanbul-lib-instrument istanbul-lib-coverage istanbul-lib-report istanbul-reports +// The __PW_COVERAGE__ define lets the sources ship test-only code, e.g. the coverage +// route of the trace viewer service worker, that a normal build drops. +export function istanbul(): Plugin { + const enabled = !!process.env.PWTEST_COVERAGE; + const config = () => ({ define: { __PW_COVERAGE__: JSON.stringify(enabled) } }); + if (!enabled) + return { name: 'playwright-istanbul', config }; + let createInstrumenter: any; + let parserPlugins: string[]; + return { + name: 'playwright-istanbul', + // Instrument the original sources, so that the counters point at them directly. + enforce: 'pre', + config, + async buildStart() { + // Not repo dependencies, resolved at run time only. + try { + createInstrumenter = (await import('istanbul-lib-instrument' as string)).createInstrumenter; + parserPlugins = (await import('@istanbuljs/schema' as string)).defaults.instrumenter.parserPlugins; + } catch (error) { + // Installed without saving, any npm install removes them again. + throw new Error(`PWTEST_COVERAGE needs the istanbul packages, run:\n npm i --no-save istanbul-lib-instrument istanbul-lib-coverage istanbul-lib-report istanbul-reports\n${error.message}`); + } + }, + transform(code, id) { + const file = id.split('?')[0]; + if (!/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) + return; + // The option replaces the default parser plugins rather than extending them. + const plugins = [...parserPlugins]; + if (/\.tsx?$/.test(file)) + plugins.push('typescript'); + if (/\.[jt]sx$/.test(file)) + plugins.push('jsx'); + const instrumenter = createInstrumenter({ esModules: true, produceSourceMap: true, parserPlugins: plugins }); + return { code: instrumenter.instrumentSync(code, file), map: instrumenter.lastSourceMap() }; + }, + }; +} From 886207b0dcaa3613ea6228fbcfe70d36bbd7728a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 16:31:56 -0700 Subject: [PATCH 2/7] chore(coverage): make the istanbul instrumenter a dev dependency The plugin imports it directly instead of resolving it at run time, and the install instructions go away. --- package-lock.json | 52 +++++++++++++++++++++++++++++++++++++ package.json | 2 ++ utils/build/viteIstanbul.ts | 34 +++++++----------------- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b50e490f8147..010d4d18bc467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@eslint/compat": "2.1.0", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", + "@istanbuljs/schema": "0.1.6", "@jest/expect-utils": "30.4.1", "@modelcontextprotocol/sdk": "1.29.0", "@stylistic/eslint-plugin": "5.10.0", @@ -94,6 +95,7 @@ "graceful-fs": "4.2.11", "https-proxy-agent": "9.1.0", "ini": "7.0.0", + "istanbul-lib-instrument": "6.0.3", "jest-matcher-utils": "30.4.1", "jest-message-util": "30.4.1", "jpeg-js": "0.4.4", @@ -1993,6 +1995,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/diff-sequences": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", @@ -6688,6 +6700,46 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", diff --git a/package.json b/package.json index 3bd7df4a818f9..d0b7f5caa5f28 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@eslint/compat": "2.1.0", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", + "@istanbuljs/schema": "0.1.6", "@jest/expect-utils": "30.4.1", "@modelcontextprotocol/sdk": "1.29.0", "@stylistic/eslint-plugin": "5.10.0", @@ -137,6 +138,7 @@ "graceful-fs": "4.2.11", "https-proxy-agent": "9.1.0", "ini": "7.0.0", + "istanbul-lib-instrument": "6.0.3", "jest-matcher-utils": "30.4.1", "jest-message-util": "30.4.1", "jpeg-js": "0.4.4", diff --git a/utils/build/viteIstanbul.ts b/utils/build/viteIstanbul.ts index 5d6ab5da2f51f..2dd58074ae5ef 100644 --- a/utils/build/viteIstanbul.ts +++ b/utils/build/viteIstanbul.ts @@ -14,46 +14,32 @@ * limitations under the License. */ +import { defaults } from '@istanbuljs/schema'; +import { createInstrumenter } from 'istanbul-lib-instrument'; + import type { Plugin } from 'vite'; // Instruments the sources under packages/ for istanbul coverage when PWTEST_COVERAGE is set. -// Requires istanbul-lib-instrument, which is not a dependency of the repo: -// npm i --no-save istanbul-lib-instrument istanbul-lib-coverage istanbul-lib-report istanbul-reports +// Runs before the other transforms, so that the counters point at the original sources. // The __PW_COVERAGE__ define lets the sources ship test-only code, e.g. the coverage // route of the trace viewer service worker, that a normal build drops. export function istanbul(): Plugin { const enabled = !!process.env.PWTEST_COVERAGE; - const config = () => ({ define: { __PW_COVERAGE__: JSON.stringify(enabled) } }); - if (!enabled) - return { name: 'playwright-istanbul', config }; - let createInstrumenter: any; - let parserPlugins: string[]; return { name: 'playwright-istanbul', - // Instrument the original sources, so that the counters point at them directly. enforce: 'pre', - config, - async buildStart() { - // Not repo dependencies, resolved at run time only. - try { - createInstrumenter = (await import('istanbul-lib-instrument' as string)).createInstrumenter; - parserPlugins = (await import('@istanbuljs/schema' as string)).defaults.instrumenter.parserPlugins; - } catch (error) { - // Installed without saving, any npm install removes them again. - throw new Error(`PWTEST_COVERAGE needs the istanbul packages, run:\n npm i --no-save istanbul-lib-instrument istanbul-lib-coverage istanbul-lib-report istanbul-reports\n${error.message}`); - } - }, + config: () => ({ define: { __PW_COVERAGE__: JSON.stringify(enabled) } }), transform(code, id) { const file = id.split('?')[0]; - if (!/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) + if (!enabled || !/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) return; // The option replaces the default parser plugins rather than extending them. - const plugins = [...parserPlugins]; + const parserPlugins = [...defaults.instrumenter.parserPlugins]; if (/\.tsx?$/.test(file)) - plugins.push('typescript'); + parserPlugins.push('typescript'); if (/\.[jt]sx$/.test(file)) - plugins.push('jsx'); - const instrumenter = createInstrumenter({ esModules: true, produceSourceMap: true, parserPlugins: plugins }); + parserPlugins.push('jsx'); + const instrumenter = createInstrumenter({ esModules: true, produceSourceMap: true, parserPlugins }); return { code: instrumenter.instrumentSync(code, file), map: instrumenter.lastSourceMap() }; }, }; From ba4baf96e8c18418f3df990487326d0a76b687e6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 16:33:09 -0700 Subject: [PATCH 3/7] chore(coverage): declare the istanbul package shapes for tsc --- tsconfig.json | 2 +- utils/build/istanbul.d.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 utils/build/istanbul.d.ts diff --git a/tsconfig.json b/tsconfig.json index 6526645a8fbd5..3bf67295e214e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,7 @@ "skipLibCheck": true, }, "compileOnSave": true, - "files": ["packages/html-reporter/tests/stories.d.ts"], + "files": ["packages/html-reporter/tests/stories.d.ts", "utils/build/istanbul.d.ts"], "include": ["packages"], "exclude": [ "packages/*/lib", diff --git a/utils/build/istanbul.d.ts b/utils/build/istanbul.d.ts new file mode 100644 index 0000000000000..1de450d0cc088 --- /dev/null +++ b/utils/build/istanbul.d.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The parts of the untyped istanbul packages that the vite plugin uses. + +declare module 'istanbul-lib-instrument' { + export type InstrumenterOptions = { + esModules?: boolean; + produceSourceMap?: boolean; + parserPlugins?: string[]; + }; + export type Instrumenter = { + instrumentSync(code: string, filename: string): string; + lastSourceMap(): any; + }; + export function createInstrumenter(options?: InstrumenterOptions): Instrumenter; +} + +declare module '@istanbuljs/schema' { + export const defaults: { + instrumenter: { parserPlugins: string[] }; + }; +} From 037b269caa89a026493e86784f018183abcfce8d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 16:36:34 -0700 Subject: [PATCH 4/7] chore(coverage): trim comments --- packages/isomorphic/istanbulCoverage.ts | 1 - packages/isomorphic/trace/snapshotRenderer.ts | 2 +- packages/trace-viewer/src/sw/main.ts | 1 - tests/config/serviceWorkerCoverage.ts | 5 ++--- tests/config/traceViewerFixtures.ts | 2 +- tests/playwright-test/reporter-html.spec.ts | 2 +- utils/build/istanbul.d.ts | 2 -- utils/build/viteIstanbul.ts | 8 +++----- 8 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/isomorphic/istanbulCoverage.ts b/packages/isomorphic/istanbulCoverage.ts index 8e85b0bb95906..fb90fc1d15125 100644 --- a/packages/isomorphic/istanbulCoverage.ts +++ b/packages/isomorphic/istanbulCoverage.ts @@ -100,7 +100,6 @@ export function mergeIstanbulCoverage(into: Map, d } } -// Returns the hit counters and resets them, so that the next take is a delta. export function takeCounters(counters: { [key: string]: number }): { [key: string]: number } | undefined { let result: { [key: string]: number } | undefined; for (const key of Object.keys(counters)) { diff --git a/packages/isomorphic/trace/snapshotRenderer.ts b/packages/isomorphic/trace/snapshotRenderer.ts index c983e7305aebe..cf2e96a46a2e9 100644 --- a/packages/isomorphic/trace/snapshotRenderer.ts +++ b/packages/isomorphic/trace/snapshotRenderer.ts @@ -320,7 +320,7 @@ function generateNonce(): string { } function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) { - // Stringified into the snapshot, where coverage counters do not exist. + // Stringified into the snapshot, keep it free of counters. /* istanbul ignore next */ function applyPlaywrightAttributes(blankSnapshotUrl: string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) { // eslint-disable-next-line no-restricted-globals diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 80474abd2c7b3..e8b71478cc636 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -155,7 +155,6 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/ping') return new Response(null, { status: 200 }); - // Coverage of the worker itself, only in a build instrumented with PWTEST_COVERAGE. if (__PW_COVERAGE__ && relativePath === '/coverage') return new Response(JSON.stringify(takeCoverage()), { status: 200, headers: { 'Content-Type': 'application/json' } }); diff --git a/tests/config/serviceWorkerCoverage.ts b/tests/config/serviceWorkerCoverage.ts index 6929037d82a22..9292466c8002d 100644 --- a/tests/config/serviceWorkerCoverage.ts +++ b/tests/config/serviceWorkerCoverage.ts @@ -16,9 +16,8 @@ type PageLike = { evaluate(pageFunction: any): Promise }; -// The trace viewer service worker keeps its own counters and serves them as a delta -// from a coverage route that exists in instrumented builds only. Folds them into the -// page's counters, from where the tracing coverage reads. +// Folds the worker's counters, served by its coverage route in instrumented builds, +// into the page's, from where the tracing coverage reads. export async function mergeServiceWorkerCoverage(page: PageLike) { if (!process.env.PWTEST_COVERAGE) return; diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 5f822f01fdd2f..949dc08549bca 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -195,7 +195,7 @@ export const traceViewerFixtures: Fixtures Promise }>({ page: async ({ page }, use) => { await use(page); - // The trace viewer worker's counters are read through the page, before it is collected. + // Before the page is collected. await mergeServiceWorkerCoverage(page); }, showReport: async ({ page }, use, testInfo) => { diff --git a/utils/build/istanbul.d.ts b/utils/build/istanbul.d.ts index 1de450d0cc088..84801957ccea4 100644 --- a/utils/build/istanbul.d.ts +++ b/utils/build/istanbul.d.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -// The parts of the untyped istanbul packages that the vite plugin uses. - declare module 'istanbul-lib-instrument' { export type InstrumenterOptions = { esModules?: boolean; diff --git a/utils/build/viteIstanbul.ts b/utils/build/viteIstanbul.ts index 2dd58074ae5ef..ec9fd855a3155 100644 --- a/utils/build/viteIstanbul.ts +++ b/utils/build/viteIstanbul.ts @@ -19,10 +19,8 @@ import { createInstrumenter } from 'istanbul-lib-instrument'; import type { Plugin } from 'vite'; -// Instruments the sources under packages/ for istanbul coverage when PWTEST_COVERAGE is set. -// Runs before the other transforms, so that the counters point at the original sources. -// The __PW_COVERAGE__ define lets the sources ship test-only code, e.g. the coverage -// route of the trace viewer service worker, that a normal build drops. +// Instruments the sources under packages/ before the other transforms, so that the +// counters point at the originals. __PW_COVERAGE__ gates test-only code in the sources. export function istanbul(): Plugin { const enabled = !!process.env.PWTEST_COVERAGE; return { @@ -33,7 +31,7 @@ export function istanbul(): Plugin { const file = id.split('?')[0]; if (!enabled || !/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) return; - // The option replaces the default parser plugins rather than extending them. + // The option replaces the defaults. const parserPlugins = [...defaults.instrumenter.parserPlugins]; if (/\.tsx?$/.test(file)) parserPlugins.push('typescript'); From 6bc9d1fb68fa57863c1598ae0089b079a44bc59e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 16:42:43 -0700 Subject: [PATCH 5/7] chore(coverage): type the instrumenter through @types/istanbul-lib-instrument The schema defaults are stage-4 syntax that the parser enables anyway, so only the typescript and jsx plugins remain and the local declarations go away. --- package-lock.json | 32 +++++++++++++++++++++++++++++++- package.json | 2 +- tsconfig.json | 2 +- utils/build/istanbul.d.ts | 34 ---------------------------------- utils/build/viteIstanbul.ts | 6 ++---- 5 files changed, 35 insertions(+), 41 deletions(-) delete mode 100644 utils/build/istanbul.d.ts diff --git a/package-lock.json b/package-lock.json index 010d4d18bc467..f5b3beacc6764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,6 @@ "@eslint/compat": "2.1.0", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", - "@istanbuljs/schema": "0.1.6", "@jest/expect-utils": "30.4.1", "@modelcontextprotocol/sdk": "1.29.0", "@stylistic/eslint-plugin": "5.10.0", @@ -55,6 +54,7 @@ "@types/diff": "7.0.2", "@types/formidable": "2.0.6", "@types/ini": "4.1.1", + "@types/istanbul-lib-instrument": "1.7.8", "@types/node": "20.19.43", "@types/pngjs": "6.0.5", "@types/progress": "2.0.7", @@ -2810,6 +2810,23 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/babel-generator": { + "version": "6.25.8", + "resolved": "https://registry.npmjs.org/@types/babel-generator/-/babel-generator-6.25.8.tgz", + "integrity": "sha512-f5l89J0UpYhTE6TFCxy3X+8pJVru1eig1fcvF9qHmOk9h1VxZimd+++tu5GShntCOdhE/MoZZ0SlpGTyh4XrKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel-types": "*" + } + }, + "node_modules/@types/babel-types": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.16.tgz", + "integrity": "sha512-5QXs9GBFTNTmilLlWBhnsprqpjfrotyrnzUdwDrywEL/DA4LuCWQT300BTOXA3Y9ngT9F2uvmCoIxI6z8DlJEA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chrome": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.2.0.tgz", @@ -2903,6 +2920,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-instrument": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.8.tgz", + "integrity": "sha512-y8t6GUkn5bTXry7Zu/2HjTakxLNtvKbIQnAiGR2M3orrdZF+zp1J9ZAKfj3VM1k3sJodkjEcWfdCJ0bEAKp6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel-generator": "*", + "@types/babel-types": "*", + "@types/istanbul-lib-coverage": "*", + "source-map": "^0.6.1" + } + }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", diff --git a/package.json b/package.json index d0b7f5caa5f28..8c112d5b90358 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "@eslint/compat": "2.1.0", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", - "@istanbuljs/schema": "0.1.6", "@jest/expect-utils": "30.4.1", "@modelcontextprotocol/sdk": "1.29.0", "@stylistic/eslint-plugin": "5.10.0", @@ -98,6 +97,7 @@ "@types/diff": "7.0.2", "@types/formidable": "2.0.6", "@types/ini": "4.1.1", + "@types/istanbul-lib-instrument": "1.7.8", "@types/node": "20.19.43", "@types/pngjs": "6.0.5", "@types/progress": "2.0.7", diff --git a/tsconfig.json b/tsconfig.json index 3bf67295e214e..6526645a8fbd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,7 @@ "skipLibCheck": true, }, "compileOnSave": true, - "files": ["packages/html-reporter/tests/stories.d.ts", "utils/build/istanbul.d.ts"], + "files": ["packages/html-reporter/tests/stories.d.ts"], "include": ["packages"], "exclude": [ "packages/*/lib", diff --git a/utils/build/istanbul.d.ts b/utils/build/istanbul.d.ts deleted file mode 100644 index 84801957ccea4..0000000000000 --- a/utils/build/istanbul.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module 'istanbul-lib-instrument' { - export type InstrumenterOptions = { - esModules?: boolean; - produceSourceMap?: boolean; - parserPlugins?: string[]; - }; - export type Instrumenter = { - instrumentSync(code: string, filename: string): string; - lastSourceMap(): any; - }; - export function createInstrumenter(options?: InstrumenterOptions): Instrumenter; -} - -declare module '@istanbuljs/schema' { - export const defaults: { - instrumenter: { parserPlugins: string[] }; - }; -} diff --git a/utils/build/viteIstanbul.ts b/utils/build/viteIstanbul.ts index ec9fd855a3155..e10b0950dddb3 100644 --- a/utils/build/viteIstanbul.ts +++ b/utils/build/viteIstanbul.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { defaults } from '@istanbuljs/schema'; import { createInstrumenter } from 'istanbul-lib-instrument'; import type { Plugin } from 'vite'; @@ -31,14 +30,13 @@ export function istanbul(): Plugin { const file = id.split('?')[0]; if (!enabled || !/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) return; - // The option replaces the defaults. - const parserPlugins = [...defaults.instrumenter.parserPlugins]; + const parserPlugins: string[] = []; if (/\.tsx?$/.test(file)) parserPlugins.push('typescript'); if (/\.[jt]sx$/.test(file)) parserPlugins.push('jsx'); const instrumenter = createInstrumenter({ esModules: true, produceSourceMap: true, parserPlugins }); - return { code: instrumenter.instrumentSync(code, file), map: instrumenter.lastSourceMap() }; + return { code: instrumenter.instrumentSync(code, file), map: { ...instrumenter.lastSourceMap(), version: 3 } }; }, }; } From 0ee491bfdff55fbc3423322a4378b3906a140bfc Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 18:03:48 -0700 Subject: [PATCH 6/7] chore(coverage): register the istanbul plugin from the vite configs The plugin no longer checks the environment or defines __PW_COVERAGE__, the worker's coverage route answers only when counters exist. --- packages/html-reporter/vite.config.ts | 2 +- packages/trace-viewer/src/sw/main.ts | 2 +- packages/trace-viewer/src/vite-env.d.ts | 1 - packages/trace-viewer/vite.config.ts | 2 +- utils/build/viteIstanbul.ts | 6 ++---- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/html-reporter/vite.config.ts b/packages/html-reporter/vite.config.ts index d4a17f7de4c2f..a73587e9d1d9d 100644 --- a/packages/html-reporter/vite.config.ts +++ b/packages/html-reporter/vite.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ react(), bundle(), storyTypes({ prefix: packageJSON.name, src: path.resolve(__dirname, 'src'), outFile: path.resolve(__dirname, 'tests/stories.d.ts') }), - istanbul(), + ...(process.env.PWTEST_COVERAGE ? [istanbul()] : []), ], resolve: { alias: { diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index e8b71478cc636..ba0957583a859 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -155,7 +155,7 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/ping') return new Response(null, { status: 200 }); - if (__PW_COVERAGE__ && relativePath === '/coverage') + if (relativePath === '/coverage' && (self as any).__coverage__) return new Response(JSON.stringify(takeCoverage()), { status: 200, headers: { 'Content-Type': 'application/json' } }); const isNavigation = !!event.resultingClientId; diff --git a/packages/trace-viewer/src/vite-env.d.ts b/packages/trace-viewer/src/vite-env.d.ts index dfe2d023f74cb..dbb4c627d26fe 100644 --- a/packages/trace-viewer/src/vite-env.d.ts +++ b/packages/trace-viewer/src/vite-env.d.ts @@ -1,4 +1,3 @@ /// declare const __APP_VERSION__: string; -declare const __PW_COVERAGE__: boolean; diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index 6089f7b60456a..56a9cb03e5abc 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ plugins: [ react(), bundle(), - istanbul(), + ...(process.env.PWTEST_COVERAGE ? [istanbul()] : []), ], define: { 'process.env': {}, diff --git a/utils/build/viteIstanbul.ts b/utils/build/viteIstanbul.ts index e10b0950dddb3..0c8f09e378b12 100644 --- a/utils/build/viteIstanbul.ts +++ b/utils/build/viteIstanbul.ts @@ -19,16 +19,14 @@ import { createInstrumenter } from 'istanbul-lib-instrument'; import type { Plugin } from 'vite'; // Instruments the sources under packages/ before the other transforms, so that the -// counters point at the originals. __PW_COVERAGE__ gates test-only code in the sources. +// counters point at the originals. export function istanbul(): Plugin { - const enabled = !!process.env.PWTEST_COVERAGE; return { name: 'playwright-istanbul', enforce: 'pre', - config: () => ({ define: { __PW_COVERAGE__: JSON.stringify(enabled) } }), transform(code, id) { const file = id.split('?')[0]; - if (!enabled || !/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) + if (!/\/packages\/.*\.[jt]sx?$/.test(file) || file.endsWith('.d.ts') || file.includes('/node_modules/')) return; const parserPlugins: string[] = []; if (/\.tsx?$/.test(file)) From 3994ed51b310165a2401d8b9b5847c30d2c8df76 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 18 Sep 2026 18:06:13 -0700 Subject: [PATCH 7/7] chore(coverage): drop the counter check from the worker's coverage route --- packages/trace-viewer/src/sw/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index ba0957583a859..6042fc81f3fd5 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -155,7 +155,7 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/ping') return new Response(null, { status: 200 }); - if (relativePath === '/coverage' && (self as any).__coverage__) + if (relativePath === '/coverage') return new Response(JSON.stringify(takeCoverage()), { status: 200, headers: { 'Content-Type': 'application/json' } }); const isNavigation = !!event.resultingClientId;