diff --git a/README.md b/README.md index 817f31e..46019bc 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,21 @@ export async function start() { } ``` +### Vite + +If you are using Vite, source maps must be enabled for the build that serves your browser tests. + +```js +// vite.config.mjs +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + sourcemap: true, + }, +}); +``` + ## Configuration ### Testem @@ -74,6 +89,12 @@ require("testem-code-coverage").middleware({ */ outputFolder: "coverage", + /** + * Path to the built assets that Chrome loads during the test run. + * Defaults to "dist". + */ + distDir: "dist", + /** * Paths to include in the coverage report. * By default, `node_modules` are excluded. @@ -94,6 +115,19 @@ require("testem-code-coverage").middleware({ */ exclude: ["**/tests/**", "**/node_modules/**", "**/.embroider/**", "**/embroider-implicit-modules/**", "**/-embroider-*"], + /** + * Built-in Istanbul reporters to run. + * + * Defaults to ["text", "html", "json-summary"]. + * + * Any reporter name supported by istanbul-reports can be used here, + * for example: "lcov", "cobertura", "json", or "text-summary". + * + * When omitted, the default behavior is preserved, including writing + * coverage/coverage-summary.txt via the text reporter. + */ + reporters: ["text", "html", "json-summary"], + /** * async callback that can be used to generate additional * report formats. @@ -120,9 +154,32 @@ require("testem-code-coverage").middleware({ */ remoteDebuggingPort: 9222, }, + + /** + * When true, write middleware diagnostics to stderr and coverage/errors.log. + */ + debug: false, +}); +``` + +### Reporter selection + +Use `reporters` when you want to choose which built-in Istanbul outputs are written. + +```js +require("testem-code-coverage").middleware({ + reporters: ["html", "json-summary", "lcov"], }); ``` +- `reporters` accepts reporter names as strings. +- Any reporter supported by `istanbul-reports` can be used. +- Omitting `reporters` preserves the current default outputs: terminal `text`, `html`, `json-summary`, and `coverage-summary.txt`. +- Setting `reporters` replaces the defaults entirely. +- If `text` is included, the middleware also writes `coverage/coverage-summary.txt`. + +Use `handleReport` only when you need custom post-processing beyond Istanbul's built-in reporters. + ## Caveats about the implementation details These are all internal things to this testem-code-coverage library diff --git a/src/testem/middleware.js b/src/testem/middleware.js index f1462c3..f26759e 100644 --- a/src/testem/middleware.js +++ b/src/testem/middleware.js @@ -70,6 +70,24 @@ import { REPORT_TO_MIDDLEWARE_PATH } from "#utils"; const CHECK_INTERVAL = 500; // ms +function normalizeReporters(reporters) { + if (reporters === undefined) return undefined; + + if (!Array.isArray(reporters)) { + throw new TypeError("[coverage] reporters must be an array of Istanbul reporter names."); + } + + const normalized = reporters.map((reporter) => { + if (typeof reporter !== "string" || reporter.trim() === "") { + throw new TypeError("[coverage] reporters must only contain non-empty strings."); + } + + return reporter.trim(); + }); + + return [...new Set(normalized)]; +} + export function middleware(options = {}) { const { outputFolder = "coverage", @@ -79,8 +97,10 @@ export function middleware(options = {}) { exclude, chrome, debug = false, + reporters, } = options; const { connectionTimeout = 30_000, remoteDebuggingPort = 9222 } = chrome || {}; + const normalizedReporters = normalizeReporters(reporters); const cwd = process.cwd(); let cdpClient = null; @@ -313,7 +333,10 @@ export function middleware(options = {}) { try { const session = createSessionClient(browser, sessionId); await session.Profiler.enable(); - await session.Profiler.startPreciseCoverage({ callCount: true, detailed: true }); + await session.Profiler.startPreciseCoverage({ + callCount: true, + detailed: true, + }); logInfo("attachedToTarget", `coverage started on session ${sessionId}`); if (waitingForDebugger) { @@ -413,7 +436,9 @@ export function middleware(options = {}) { // Open fresh tab at the test URL. waitForDebuggerOnStart pauses it // before any JS, giving us the waitingForDebugger=true path above // for coverage setup (cache clear + resume). - const { targetId } = await browser.Target.createTarget({ url: testUrl }); + const { targetId } = await browser.Target.createTarget({ + url: testUrl, + }); coverageTabTargetId = targetId; logInfo( "attachedToTarget", @@ -563,6 +588,7 @@ export function middleware(options = {}) { include, exclude, debug, + reporters: normalizedReporters, }); await handleReport?.(coverageResult); diff --git a/src/v8/report.js b/src/v8/report.js index c3f436f..947b8d8 100644 --- a/src/v8/report.js +++ b/src/v8/report.js @@ -14,6 +14,8 @@ import libReport from "istanbul-lib-report"; import reports from "istanbul-reports"; import picomatch from "picomatch"; +const DEFAULT_REPORTERS = ["text", "html", "json-summary"]; + /** * Resolve package names (e.g. "my-addon", "@scope/pkg") to their absolute * directories, using `createRequire` anchored at the project root @@ -262,7 +264,11 @@ function syntheticUncoveredMethods( diag( ` MethodDef ${methodLabel} @${node.start} (key@${keyStart}) NOT in V8 — local=true source=${origSource}`, ); - localMethodRanges.push({ label: methodLabel, start: node.start, end: node.end }); + localMethodRanges.push({ + label: methodLabel, + start: node.start, + end: node.end, + }); synthetic.push({ functionName: methodLabel, // Use the full MethodDefinition range so the synthetic entry spans the @@ -344,6 +350,8 @@ export async function generateReport(v8Scripts, options = {}) { const coverageDir = options.coverageDir ?? path.join(process.cwd(), "coverage"); const cwd = process.cwd(); const excludePatterns = options.exclude ?? DEFAULT_EXCLUDE; + const configuredReporters = options.reporters; + const effectiveReporters = configuredReporters ?? DEFAULT_REPORTERS; const isExcluded = excludePatterns.length > 0 ? picomatch(excludePatterns) : () => false; // Resolve any explicitly included package names to their directories @@ -493,17 +501,29 @@ export async function generateReport(v8Scripts, options = {}) { }, }); - // Terminal output + if (effectiveReporters.length === 0) { + console.log("\n[coverage] No Istanbul reporters configured."); + return; + } + + const shouldWriteTextSummaryFile = + configuredReporters === undefined || effectiveReporters.includes("text"); + console.log("\n"); - reports.create("text").execute(context); - - // HTML + JSON summary reports - reports.create("html").execute(context); - // json-summary writes coverage-summary.json — consumed by integration tests. - reports.create("json-summary").execute(context); - // text report written to file mirrors the terminal table output. - reports.create("text", { file: "coverage-summary.txt" }).execute(context); - console.log(`\nHTML coverage report → ${path.join(coverageDir, "index.html")}\n`); + + for (const reporterName of effectiveReporters) { + reports.create(reporterName).execute(context); + } + + // Preserve the legacy text summary file for the default path, and when + // users explicitly request the text reporter. + if (shouldWriteTextSummaryFile) { + reports.create("text", { file: "coverage-summary.txt" }).execute(context); + } + + if (effectiveReporters.includes("html")) { + console.log(`\nHTML coverage report → ${path.join(coverageDir, "index.html")}\n`); + } } /** diff --git a/test-scenarios/vite-app-js/testem.cjs b/test-scenarios/vite-app-js/testem.cjs index b3ced88..c92155a 100644 --- a/test-scenarios/vite-app-js/testem.cjs +++ b/test-scenarios/vite-app-js/testem.cjs @@ -1,33 +1,31 @@ -"use strict"; +'use strict'; -if (typeof module !== "undefined") { +if (typeof module !== 'undefined') { module.exports = { - test_page: "tests/index.html?hidepassed", - cwd: "dist", + test_page: 'tests/index.html?hidepassed', + cwd: 'dist', disable_watching: true, - launch_in_ci: ["Chrome"], - launch_in_dev: ["Chrome"], + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], browser_start_timeout: 120, middleware: [ - require("testem-code-coverage").middleware({ - /* options here */ + require('testem-code-coverage').middleware({ + reporters: ['json-summary', 'lcov'], }), ], browser_args: { Chrome: { ci: [ // --no-sandbox is needed when running Chrome inside a container - process.env.CI ? "--no-sandbox" : null, - "--headless", - "--disable-dev-shm-usage", - "--disable-software-rasterizer", - "--mute-audio", - "--remote-debugging-port=9222", - "--window-size=1440,900", + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=9222', + '--window-size=1440,900', ].filter(Boolean), - dev: [ - "--remote-debugging-port=9222", - ], + dev: ['--remote-debugging-port=9222'], }, }, }; diff --git a/tests/vite-app-js.test.js b/tests/vite-app-js.test.js index 323346b..3bb070a 100644 --- a/tests/vite-app-js.test.js +++ b/tests/vite-app-js.test.js @@ -18,6 +18,22 @@ test("coverage directory was created", () => { expect(existsSync(join(scenarioDir, "coverage")), "coverage directory was created").toBe(true); }); +test("custom reporters only write the requested artifacts", () => { + expect( + existsSync(join(scenarioDir, "coverage", "coverage-summary.json")), + "json-summary output exists", + ).toBe(true); + expect(existsSync(join(scenarioDir, "coverage", "lcov.info")), "lcov output exists").toBe(true); + expect( + existsSync(join(scenarioDir, "coverage", "coverage-summary.txt")), + "text summary is omitted when text reporter is not configured", + ).toBe(false); + expect( + existsSync(join(scenarioDir, "coverage", "index.html")), + "html report is omitted when html reporter is not configured", + ).toBe(false); +}); + // vite-app-js currently only produces coverage for embroider virtual files // (dist/@embroider/virtual/*), not for actual app source files. // This is a known limitation — the scenario validates that the coverage