Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down
30 changes: 28 additions & 2 deletions src/testem/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -563,6 +588,7 @@ export function middleware(options = {}) {
include,
exclude,
debug,
reporters: normalizedReporters,
});

await handleReport?.(coverageResult);
Expand Down
42 changes: 31 additions & 11 deletions src/v8/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`);
}
}

/**
Expand Down
34 changes: 16 additions & 18 deletions test-scenarios/vite-app-js/testem.cjs
Original file line number Diff line number Diff line change
@@ -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'],
},
},
};
Expand Down
16 changes: 16 additions & 0 deletions tests/vite-app-js.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading