diff --git a/packages/jest/src/default-config.resolver.ts b/packages/jest/src/default-config.resolver.ts index 022bce823..a3a4df00f 100644 --- a/packages/jest/src/default-config.resolver.ts +++ b/packages/jest/src/default-config.resolver.ts @@ -1,5 +1,6 @@ import { pick } from 'lodash'; import { getSystemPath, normalize, Path } from '@angular-devkit/core'; +import * as path from 'node:path'; import { JestConfig } from './types'; import { getTsConfigPath } from './utils'; @@ -39,6 +40,9 @@ export class DefaultConfigResolver { resolveForProject(projectRoot: Path): JestConfig { return { testMatch: [`${getSystemPath(projectRoot)}${testPattern}`], + // Scope coverage output to the project directory so that multiple projects + // in a workspace don't overwrite each other's coverage reports. See #1009. + coverageDirectory: path.join(getSystemPath(projectRoot), 'coverage'), transform: { [this.tsJestTransformRegExp]: [ 'jest-preset-angular', diff --git a/packages/jest/tests/integration.js b/packages/jest/tests/integration.js index 117a6a954..d10e17e34 100644 --- a/packages/jest/tests/integration.js +++ b/packages/jest/tests/integration.js @@ -142,6 +142,14 @@ module.exports = [ app: '.', command: 'node scripts/e2e-jest-migration.js', }, + { + id: 'multi-project-coverage', + name: 'jest: multi-project coverage scoping', + purpose: 'Coverage output is scoped per project, not overwritten', + app: 'examples/jest/multiple-apps', + command: + 'node ../../../packages/jest/tests/validate-coverage.js', + }, // E2E sanity { diff --git a/packages/jest/tests/validate-coverage.js b/packages/jest/tests/validate-coverage.js new file mode 100644 index 000000000..6fe7e62e6 --- /dev/null +++ b/packages/jest/tests/validate-coverage.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Validates that coverage output is scoped per Angular project (fixes #1009). + * + * Each project should write coverage to /coverage/, not to a shared + * root-level directory that gets overwritten when multiple projects run. + * + * Run from the multiple-apps example directory. + */ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const projects = ['my-first-app', 'my-second-app', 'my-shared-library']; +const cwd = process.cwd(); +let allPassed = true; + +for (const project of projects) { + console.log(`\nRunning: ng test ${project} --coverage`); + try { + execSync(`yarn test ${project} --coverage 2>&1`, { encoding: 'utf-8', stdio: 'pipe' }); + } catch (e) { + // Jest exits non-zero on test failure but still writes coverage; ignore for this check + } + + // Angular CLI project root is under `projects/` + const projectRoot = path.join(cwd, 'projects', project); + const coverageDir = path.join(projectRoot, 'coverage'); + + if (!fs.existsSync(coverageDir)) { + console.error(`FAIL: Expected coverage directory not found: ${coverageDir}`); + console.error(` (coverage was likely written to root ./coverage instead)`); + allPassed = false; + } else { + console.log(`OK: Coverage scoped correctly at ${coverageDir}`); + } +} + +// Also assert that no root-level coverage dir was created (it would indicate the fix didn't work) +const rootCoverageDir = path.join(cwd, 'coverage'); +if (fs.existsSync(rootCoverageDir)) { + console.warn(`WARN: Root-level coverage/ directory exists at ${rootCoverageDir}`); + console.warn(` This may indicate per-project scoping is not fully working.`); +} + +if (!allPassed) { + process.exit(1); +} +console.log('\nAll per-project coverage directories verified correctly.');