@zohodesk/unit-testing-framework 0.0.16-experimental → 0.0.18-experimental

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -3,24 +3,6 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- Object.defineProperty(exports, "CoverageReporter", {
7
- enumerable: true,
8
- get: function () {
9
- return _coverageReporter.default;
10
- }
11
- });
12
- Object.defineProperty(exports, "applyCoverageOverrides", {
13
- enumerable: true,
14
- get: function () {
15
- return _coverageConfig.applyCoverageOverrides;
16
- }
17
- });
18
- Object.defineProperty(exports, "createCoverageRunner", {
19
- enumerable: true,
20
- get: function () {
21
- return _coverageRunner.default;
22
- }
23
- });
24
6
  Object.defineProperty(exports, "createJestRunner", {
25
7
  enumerable: true,
26
8
  get: function () {
@@ -33,20 +15,5 @@ Object.defineProperty(exports, "default", {
33
15
  return _jestRunner.default;
34
16
  }
35
17
  });
36
- Object.defineProperty(exports, "getCoverageDefaults", {
37
- enumerable: true,
38
- get: function () {
39
- return _coverageConfig.getCoverageDefaults;
40
- }
41
- });
42
- Object.defineProperty(exports, "isCoverageEnabled", {
43
- enumerable: true,
44
- get: function () {
45
- return _coverageConfig.isCoverageEnabled;
46
- }
47
- });
48
18
  var _jestRunner = _interopRequireDefault(require("./src/runner/jest-runner.js"));
49
- var _coverageRunner = _interopRequireDefault(require("./src/coverage/coverage-runner.js"));
50
- var _coverageReporter = _interopRequireDefault(require("./src/reporters/coverage-reporter.js"));
51
- var _coverageConfig = require("./src/coverage/coverage-config.js");
52
19
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -31,9 +31,9 @@ function getDefaultConfig(projectRoot) {
31
31
  // __tests__ folders can be nested anywhere under the project,
32
32
  // so we root Jest at the project itself and rely on testMatch
33
33
  // to locate **/__tests__/**/* files at any depth.
34
- roots: [projectRoot],
34
+ rootDir: process.cwd(),
35
35
  testMatch: ['**/__tests__/**/*.test.js'],
36
- testPathIgnorePatterns: ['/node_modules/', '/build/'],
36
+ testPathIgnorePatterns: ['/node_modules/', '/build/', '/uat/'],
37
37
  // --------------- Environment ---------------
38
38
  testEnvironment: 'jsdom',
39
39
  // --------------- Transform ---------------
@@ -51,17 +51,8 @@ function getDefaultConfig(projectRoot) {
51
51
  }]
52
52
  },
53
53
  transformIgnorePatterns: ['/node_modules/'],
54
- // --------------- Coverage ---------------
55
- // Coverage defaults are managed by CoverageManager
56
- // (src/coverage/coverage-config.js). The runner merges
57
- // them in automatically; consumers override via the
58
- // `coverage` option passed to createJestRunner().
59
-
60
54
  // --------------- Reporters ---------------
61
- reporters: ['default', 'html-report'
62
- // 'coverage-report' is injected automatically by
63
- // CoverageManager when coverage is enabled.
64
- ],
55
+ reporters: ['default', 'html-report'],
65
56
  // --------------- Parallelism & Performance ---------------
66
57
  maxWorkers: '50%',
67
58
  // Use half of available CPUs
@@ -74,13 +65,12 @@ function getDefaultConfig(projectRoot) {
74
65
  // Injects `jest` into globalThis so ESM test files don't need
75
66
  // `import { jest } from '@jest/globals'` manually.
76
67
  setupFiles: [_path.default.resolve(__dirname, '..', 'environment', 'globals-inject.js')],
77
- // --------------- Global Setup / Teardown ---------------
78
- globalSetup: _path.default.resolve(__dirname, '..', 'environment', 'setup.js'),
79
- globalTeardown: _path.default.resolve(__dirname, '..', 'environment', 'teardown.js'),
80
68
  // --------------- Module Resolution ---------------
81
69
  moduleFileExtensions: ['js', 'mjs', 'ts', 'json', 'node'],
82
70
  // --------------- Misc ---------------
83
71
  verbose: true,
84
- passWithNoTests: true
72
+ passWithNoTests: true,
73
+ clearMocks: true,
74
+ resetMocks: false
85
75
  };
86
76
  }
@@ -5,26 +5,5 @@ class configConstants {
5
5
  static UNIT_CONFIG_FILE = 'unit.config.js';
6
6
  static STAGE_CONFIG_MAP_FILE = 'test-slices/conf_path_map.properties';
7
7
  static TEST_SLICE_FOLDER = 'test-slices';
8
-
9
- // Coverage
10
- static COVERAGE_DIR = 'coverage';
11
- static COVERAGE_SUMMARY_FILE = 'coverage-summary.json';
12
- static COVERAGE_REPORT_DIR = 'unit_reports';
13
- static COVERAGE_HTML_FILE = 'coverage-report.html';
14
-
15
- // Coverage thresholds (default enforcement levels)
16
- static COVERAGE_THRESHOLD_DEFAULT = {
17
- branches: 0,
18
- functions: 0,
19
- lines: 0,
20
- statements: 0
21
- };
22
-
23
- // Coverage reporter formats
24
- static COVERAGE_REPORTERS = ['text', 'text-summary', 'lcov', 'clover', 'json-summary'];
25
-
26
- // Coverage provider options: 'babel' or 'v8'
27
- static COVERAGE_PROVIDER_BABEL = 'babel';
28
- static COVERAGE_PROVIDER_V8 = 'v8';
29
8
  }
30
9
  module.exports = configConstants;
@@ -23,8 +23,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
23
23
 
24
24
  const BUILTIN_ALIASES = {
25
25
  'framework-default': _path.default.resolve(__dirname, 'default-reporter.js'),
26
- 'html-report': _path.default.resolve(__dirname, 'html-reporter.js'),
27
- 'coverage-report': _path.default.resolve(__dirname, 'coverage-reporter.js')
26
+ 'html-report': _path.default.resolve(__dirname, 'html-reporter.js')
28
27
  };
29
28
 
30
29
  /**
@@ -12,10 +12,8 @@ var _reporterHandler = require("../reporters/reporter-handler.js");
12
12
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
13
13
  * runner-base.js
14
14
  *
15
- * Shared utilities used by both jest-runner.js (tests only)
16
- * and coverage-runner.js (tests + coverage).
17
- *
18
- * Keeps common logic in one place so neither runner duplicates it.
15
+ * Shared utilities for the Jest test runner.
16
+ * Keeps common logic in one place for reuse.
19
17
  */
20
18
  /**
21
19
  * Create the base Jest config with common CLI overrides applied.
@@ -56,15 +54,13 @@ function resolveConfigReporters(config, projectRoot) {
56
54
  * @param {string} [params.testPathPattern] - Regex to filter test files.
57
55
  * @param {boolean} [params.watch] - Watch mode flag.
58
56
  * @param {string} params.projectRoot - Consumer project root.
59
- * @param {object} [params.argvOverrides] - Extra keys merged into argv (e.g. { collectCoverage: true }).
60
57
  * @returns {object}
61
58
  */
62
59
  function buildArgv(config, {
63
60
  testFiles,
64
61
  testPathPattern,
65
62
  watch = false,
66
- projectRoot,
67
- argvOverrides = {}
63
+ projectRoot
68
64
  }) {
69
65
  const argv = {
70
66
  // Serialise the config so Jest uses our merged config
@@ -78,15 +74,12 @@ function buildArgv(config, {
78
74
  ci: process.env.CI === 'true',
79
75
  // Pass-through values that CLI users might expect
80
76
  verbose: config.verbose ?? true,
81
- collectCoverage: config.collectCoverage ?? false,
82
77
  passWithNoTests: config.passWithNoTests ?? true,
83
78
  maxWorkers: config.maxWorkers ?? '50%',
84
79
  silent: config.silent ?? false,
85
80
  // Do not search for config files automatically
86
81
  _: testFiles ?? [],
87
- $0: 'unit-testing-framework',
88
- // Caller-specific overrides (e.g. coverage runner forces collectCoverage: true)
89
- ...argvOverrides
82
+ $0: 'unit-testing-framework'
90
83
  };
91
84
  if (testPathPattern) {
92
85
  argv.testPathPattern = testPathPattern;
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@zohodesk/unit-testing-framework",
3
- "version": "0.0.16-experimental",
3
+ "version": "0.0.18-experimental",
4
4
  "description": "A modular Jest-based unit testing framework",
5
5
  "main": "./build/index.js",
6
6
  "exports": {
7
7
  ".": "./build/index.js",
8
8
  "./config": "./build/src/config/default-config.js",
9
9
  "./reporters": "./build/src/reporters/reporter-handler.js",
10
- "./runner": "./build/src/runner/jest-runner.js",
11
- "./coverage": "./build/src/coverage/coverage-runner.js"
10
+ "./runner": "./build/src/runner/jest-runner.js"
12
11
  },
13
12
  "files": [
14
13
  "build/"
@@ -1,77 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.applyCoverageOverrides = applyCoverageOverrides;
7
- exports.getCoverageDefaults = getCoverageDefaults;
8
- exports.isCoverageEnabled = isCoverageEnabled;
9
- var _path = _interopRequireDefault(require("path"));
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- /**
12
- * coverage-config.js
13
- *
14
- * Isolated coverage configuration module.
15
- * Keeps all coverage-related defaults and overrides separate
16
- * from the core test runner, so the runner stays single-responsibility.
17
- *
18
- * Usage:
19
- * import { getCoverageDefaults, applyCoverageOverrides } from './coverage-config.js';
20
- */
21
-
22
- /**
23
- * @typedef {object} CoverageOptions
24
- * @property {boolean} [enabled] - Whether to collect coverage.
25
- * @property {string} [directory] - Output directory for coverage data.
26
- * @property {string[]} [reporters] - Jest coverage reporter formats (e.g. ['text', 'lcov']).
27
- * @property {string[]} [collectFrom] - Glob patterns for files to instrument.
28
- * @property {object} [threshold] - Minimum coverage percentages to enforce.
29
- * @property {string} [provider] - Instrumentation provider: 'babel' | 'v8'.
30
- * @property {string[]} [ignorePatterns] - Patterns to exclude from coverage.
31
- */
32
-
33
- /**
34
- * Returns the default coverage-specific Jest config keys.
35
- *
36
- * @param {string} projectRoot - Absolute path to the consumer project root.
37
- * @returns {import('@jest/types').Config.InitialOptions} Coverage-only config slice.
38
- */
39
- function getCoverageDefaults(projectRoot) {
40
- return {
41
- collectCoverage: false,
42
- coverageDirectory: _path.default.resolve(projectRoot, 'coverage'),
43
- coverageReporters: ['text', 'text-summary', 'lcov', 'clover', 'json-summary'],
44
- coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/', '/tests/', '/test/', '\\.test\\.[jt]sx?$', '\\.spec\\.[jt]sx?$', '/build/', '/dist/'],
45
- collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/**/__tests__/**', '!src/**/*.test.{js,jsx,ts,tsx}', '!src/**/*.spec.{js,jsx,ts,tsx}'],
46
- coverageProvider: 'babel',
47
- coverageThreshold: null
48
- };
49
- }
50
-
51
- /**
52
- * Apply consumer-supplied coverage overrides onto a config object.
53
- * Only sets values that were explicitly provided (not undefined).
54
- *
55
- * @param {object} config - Mutable Jest config to patch.
56
- * @param {CoverageOptions} opts - Consumer coverage overrides.
57
- * @param {string} projectRoot - Consumer project root (for path resolution).
58
- */
59
- function applyCoverageOverrides(config, opts = {}, projectRoot = process.cwd()) {
60
- if (opts.enabled !== undefined) config.collectCoverage = opts.enabled;
61
- if (opts.directory !== undefined) config.coverageDirectory = _path.default.resolve(projectRoot, opts.directory);
62
- if (opts.reporters !== undefined) config.coverageReporters = opts.reporters;
63
- if (opts.collectFrom !== undefined) config.collectCoverageFrom = opts.collectFrom;
64
- if (opts.threshold !== undefined) config.coverageThreshold = opts.threshold;
65
- if (opts.provider !== undefined) config.coverageProvider = opts.provider;
66
- if (opts.ignorePatterns !== undefined) config.coveragePathIgnorePatterns = opts.ignorePatterns;
67
- }
68
-
69
- /**
70
- * Check whether a given config object has coverage enabled.
71
- *
72
- * @param {object} config - Jest config.
73
- * @returns {boolean}
74
- */
75
- function isCoverageEnabled(config) {
76
- return config.collectCoverage === true;
77
- }
@@ -1,88 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.CoverageManager = void 0;
7
- var _coverageConfig = require("./coverage-config.js");
8
- /**
9
- * coverage-manager.js
10
- *
11
- * Orchestrates all coverage concerns:
12
- * 1. Merging coverage defaults into the Jest config
13
- * 2. Applying consumer overrides
14
- * 3. Conditionally injecting the coverage reporter
15
- *
16
- * The runner delegates to this module instead of handling
17
- * coverage logic inline, keeping the runner single-responsibility.
18
- *
19
- * Usage (inside jest-runner.js):
20
- * import { CoverageManager } from '../coverage/coverage-manager.js';
21
- * const cm = new CoverageManager(projectRoot, coverageOpts);
22
- * cm.apply(mergedConfig);
23
- */
24
-
25
- const COVERAGE_REPORTER_ALIAS = 'coverage-report';
26
- class CoverageManager {
27
- /**
28
- * @param {string} projectRoot - Consumer project root.
29
- * @param {import('./coverage-config.js').CoverageOptions} [overrides={}]
30
- */
31
- constructor(projectRoot, overrides = {}) {
32
- this.projectRoot = projectRoot;
33
- this.overrides = overrides;
34
- }
35
-
36
- /**
37
- * Merge coverage defaults into the config, then apply
38
- * any consumer-supplied overrides.
39
- *
40
- * @param {object} config - Mutable Jest config object.
41
- * @returns {object} The same config reference (mutated).
42
- */
43
- apply(config) {
44
- // ── 1. Merge coverage defaults ──────────────────────────
45
- const defaults = (0, _coverageConfig.getCoverageDefaults)(this.projectRoot);
46
- for (const [key, value] of Object.entries(defaults)) {
47
- // Only set defaults that aren't already present in config
48
- if (config[key] === undefined) {
49
- config[key] = value;
50
- }
51
- }
52
-
53
- // ── 2. Apply consumer overrides ─────────────────────────
54
- (0, _coverageConfig.applyCoverageOverrides)(config, this.overrides, this.projectRoot);
55
-
56
- // ── 3. Conditionally add / remove coverage reporter ─────
57
- this._syncCoverageReporter(config);
58
- return config;
59
- }
60
-
61
- /**
62
- * Ensure the coverage reporter is present only when
63
- * coverage is enabled, and removed when it isn't.
64
- *
65
- * @param {object} config
66
- * @private
67
- */
68
- _syncCoverageReporter(config) {
69
- if (!Array.isArray(config.reporters)) return;
70
- const hasAlias = config.reporters.some(r => {
71
- const name = Array.isArray(r) ? r[0] : r;
72
- return name === COVERAGE_REPORTER_ALIAS;
73
- });
74
- if ((0, _coverageConfig.isCoverageEnabled)(config)) {
75
- // Inject the coverage reporter if not already present
76
- if (!hasAlias) {
77
- config.reporters.push(COVERAGE_REPORTER_ALIAS);
78
- }
79
- } else {
80
- // Strip the coverage reporter when coverage is off
81
- config.reporters = config.reporters.filter(r => {
82
- const name = Array.isArray(r) ? r[0] : r;
83
- return name !== COVERAGE_REPORTER_ALIAS;
84
- });
85
- }
86
- }
87
- }
88
- exports.CoverageManager = CoverageManager;
@@ -1,123 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = createCoverageRunner;
7
- var _runnerBase = require("../runner/runner-base.js");
8
- var _coverageConfig = require("./coverage-config.js");
9
- /**
10
- * coverage-runner.js
11
- *
12
- * Standalone module for running Jest with code coverage.
13
- * Completely independent of jest-runner.js — consumers call
14
- * this directly when they want coverage:
15
- *
16
- * import { createCoverageRunner } from 'unit-testing-framework';
17
- * await createCoverageRunner({ projectRoot: process.cwd() });
18
- *
19
- * Or with overrides:
20
- * await createCoverageRunner({
21
- * projectRoot: process.cwd(),
22
- * directory: 'reports/coverage',
23
- * threshold: { global: { lines: 80 } },
24
- * });
25
- */
26
-
27
- const COVERAGE_REPORTER_ALIAS = 'coverage-report';
28
-
29
- /**
30
- * @typedef {object} CoverageRunnerOptions
31
- * @property {string} [projectRoot] - Absolute path to the consumer project (default: cwd).
32
- * @property {string[]} [testFiles] - Specific test file patterns to run.
33
- * @property {string} [testPathPattern]- Regex to match test file paths.
34
- * @property {boolean} [verbose] - Verbose output.
35
- * @property {number|string} [maxWorkers] - Worker concurrency (e.g. '50%' or 4).
36
- * @property {boolean} [silent] - Suppress console output from tests.
37
- * @property {string} [directory] - Coverage output directory.
38
- * @property {string[]} [reporters] - Jest coverage reporter formats (e.g. ['text', 'lcov']).
39
- * @property {string[]} [collectFrom] - Glob patterns for files to instrument.
40
- * @property {object} [threshold] - Minimum coverage % per metric.
41
- * @property {string} [provider] - Instrumentation provider: 'babel' | 'v8'.
42
- * @property {string[]} [ignorePatterns] - Patterns to exclude from coverage.
43
- */
44
-
45
- /**
46
- * Run Jest with code coverage enabled.
47
- *
48
- * @param {CoverageRunnerOptions} [options={}]
49
- * @returns {Promise<import('@jest/core').AggregatedResult>} Jest aggregated results.
50
- */
51
- async function createCoverageRunner(options = {}) {
52
- const {
53
- projectRoot = process.cwd(),
54
- testFiles,
55
- testPathPattern,
56
- verbose,
57
- maxWorkers,
58
- silent,
59
- // Coverage-specific options
60
- directory,
61
- reporters,
62
- collectFrom,
63
- threshold,
64
- provider,
65
- ignorePatterns
66
- } = options;
67
-
68
- // ── 1. Build base config with overrides ────────────────────
69
- const config = (0, _runnerBase.createBaseConfig)(projectRoot, {
70
- verbose,
71
- maxWorkers,
72
- silent
73
- });
74
-
75
- // ── 2. Merge coverage defaults ─────────────────────────────
76
- const coverageDefaults = (0, _coverageConfig.getCoverageDefaults)(projectRoot);
77
- for (const [key, value] of Object.entries(coverageDefaults)) {
78
- if (config[key] === undefined) {
79
- config[key] = value;
80
- }
81
- }
82
-
83
- // Always enable coverage — that's the purpose of this runner.
84
- config.collectCoverage = true;
85
-
86
- // ── 3. Apply consumer coverage overrides ───────────────────
87
- (0, _coverageConfig.applyCoverageOverrides)(config, {
88
- enabled: true,
89
- directory,
90
- reporters,
91
- collectFrom,
92
- threshold,
93
- provider,
94
- ignorePatterns
95
- }, projectRoot);
96
-
97
- // ── 4. Inject coverage reporter if not present ─────────────
98
- if (Array.isArray(config.reporters)) {
99
- const hasCoverageReporter = config.reporters.some(r => {
100
- const name = Array.isArray(r) ? r[0] : r;
101
- return name === COVERAGE_REPORTER_ALIAS;
102
- });
103
- if (!hasCoverageReporter) {
104
- config.reporters.push(COVERAGE_REPORTER_ALIAS);
105
- }
106
- }
107
-
108
- // ── 5. Resolve reporters ───────────────────────────────────
109
- (0, _runnerBase.resolveConfigReporters)(config, projectRoot);
110
-
111
- // ── 6. Build argv & run Jest ───────────────────────────────
112
- const argv = (0, _runnerBase.buildArgv)(config, {
113
- testFiles,
114
- testPathPattern,
115
- watch: false,
116
- // No watch mode in coverage runs
117
- projectRoot,
118
- argvOverrides: {
119
- collectCoverage: true
120
- }
121
- });
122
- return (0, _runnerBase.executeJest)(argv, projectRoot);
123
- }
@@ -1,21 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = globalSetup;
7
- /**
8
- * setup.js
9
- *
10
- * Global setup module executed once before all test suites.
11
- * Runs in the parent process (not in test workers), so it's
12
- * suitable for one-time tasks like starting services or
13
- * setting environment variables.
14
- *
15
- * This file is referenced by the default config's `globalSetup`.
16
- */
17
-
18
- async function globalSetup() {
19
- // Intentionally minimal — consumers can override globalSetup
20
- // in their own config to add project-specific logic.
21
- }
@@ -1,21 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = globalTeardown;
7
- /**
8
- * teardown.js
9
- *
10
- * Global teardown module executed once after all test suites complete.
11
- * Runs in the parent process (not in test workers), so it's
12
- * suitable for cleanup tasks like stopping services or
13
- * removing temp files.
14
- *
15
- * This file is referenced by the default config's `globalTeardown`.
16
- */
17
-
18
- async function globalTeardown() {
19
- // Intentionally minimal — consumers can override globalTeardown
20
- // in their own config to add project-specific logic.
21
- }
@@ -1,407 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
- var _fs = _interopRequireDefault(require("fs"));
8
- var _path = _interopRequireDefault(require("path"));
9
- var _logger = require("../utils/logger.js");
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- /**
12
- * coverage-reporter.js
13
- *
14
- * A custom Jest reporter that produces a self-contained HTML coverage report
15
- * and enforces configurable coverage thresholds. Generates a detailed
16
- * per-file breakdown showing line, branch, function, and statement coverage.
17
- *
18
- * Usage in reporters config:
19
- * ['coverage-report'] → defaults
20
- * ['coverage-report', { outputDir: '...', thresholds: { lines: 80 } }]
21
- *
22
- * Reporter options:
23
- * - outputDir {string} Directory for the HTML file (default: <rootDir>/test-slices/unit-test/unit_reports)
24
- * - fileName {string} HTML file name (default: coverage-report.html)
25
- * - title {string} Page title (default: Code Coverage Report)
26
- * - thresholds {object} Minimum coverage % per metric (branches, functions, lines, statements)
27
- * - failOnThreshold {boolean} Exit with error if thresholds not met (default: false)
28
- */
29
-
30
- class CoverageReporter {
31
- constructor(globalConfig, reporterOptions = {}) {
32
- this.globalConfig = globalConfig;
33
- this.options = reporterOptions;
34
- }
35
- onRunComplete(_contexts, aggregatedResults) {
36
- const coverageMap = aggregatedResults.coverageMap;
37
- if (!coverageMap) {
38
- _logger.Logger.log(_logger.Logger.INFO_TYPE, '\n ⚠ Coverage data not available. Run with --coverage to collect coverage.\n');
39
- return;
40
- }
41
- const rootDir = this.globalConfig.rootDir || process.cwd();
42
- const outputDir = this.options.outputDir ? _path.default.resolve(rootDir, this.options.outputDir) : _path.default.resolve(rootDir, 'test-slices', 'unit-test', 'unit_reports');
43
- const fileName = this.options.fileName || 'coverage-report.html';
44
- const title = this.options.title || 'Code Coverage Report';
45
- const outputPath = _path.default.join(outputDir, fileName);
46
-
47
- // ── 1. Extract per-file coverage data ──────────────────────
48
- const files = coverageMap.files();
49
- const fileSummaries = files.map(filePath => {
50
- const fileCoverage = coverageMap.fileCoverageFor(filePath);
51
- const summary = fileCoverage.toSummary();
52
- return {
53
- file: _path.default.relative(rootDir, filePath),
54
- lines: summary.lines,
55
- statements: summary.statements,
56
- branches: summary.branches,
57
- functions: summary.functions
58
- };
59
- });
60
-
61
- // ── 2. Compute totals ──────────────────────────────────────
62
- const totals = this._computeTotals(fileSummaries);
63
-
64
- // ── 3. Print console summary ───────────────────────────────
65
- this._printConsoleSummary(totals, fileSummaries);
66
-
67
- // ── 4. Generate HTML report ────────────────────────────────
68
- _fs.default.mkdirSync(outputDir, {
69
- recursive: true
70
- });
71
- const html = this._generateHtml(totals, fileSummaries, title);
72
- _fs.default.writeFileSync(outputPath, html, 'utf-8');
73
- _logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `\n 📊 Coverage report written to: ${outputPath}\n`);
74
-
75
- // ── 5. Write JSON summary alongside HTML ───────────────────
76
- const jsonPath = _path.default.join(outputDir, 'coverage-summary.json');
77
- _fs.default.writeFileSync(jsonPath, JSON.stringify({
78
- totals,
79
- files: fileSummaries
80
- }, null, 2), 'utf-8');
81
-
82
- // ── 6. Enforce thresholds ──────────────────────────────────
83
- if (this.options.thresholds) {
84
- this._enforceThresholds(totals, this.options.thresholds);
85
- }
86
- }
87
-
88
- // ── Totals computation ───────────────────────────────────────
89
-
90
- _computeTotals(fileSummaries) {
91
- const sum = {
92
- lines: {
93
- total: 0,
94
- covered: 0
95
- },
96
- statements: {
97
- total: 0,
98
- covered: 0
99
- },
100
- branches: {
101
- total: 0,
102
- covered: 0
103
- },
104
- functions: {
105
- total: 0,
106
- covered: 0
107
- }
108
- };
109
- for (const f of fileSummaries) {
110
- for (const metric of ['lines', 'statements', 'branches', 'functions']) {
111
- sum[metric].total += f[metric].total;
112
- sum[metric].covered += f[metric].covered;
113
- }
114
- }
115
- const pct = (covered, total) => total === 0 ? 100 : parseFloat((covered / total * 100).toFixed(2));
116
- return {
117
- lines: {
118
- ...sum.lines,
119
- pct: pct(sum.lines.covered, sum.lines.total)
120
- },
121
- statements: {
122
- ...sum.statements,
123
- pct: pct(sum.statements.covered, sum.statements.total)
124
- },
125
- branches: {
126
- ...sum.branches,
127
- pct: pct(sum.branches.covered, sum.branches.total)
128
- },
129
- functions: {
130
- ...sum.functions,
131
- pct: pct(sum.functions.covered, sum.functions.total)
132
- }
133
- };
134
- }
135
-
136
- // ── Console summary ─────────────────────────────────────────
137
-
138
- _printConsoleSummary(totals, fileSummaries) {
139
- _logger.Logger.log(_logger.Logger.INFO_TYPE, '\n╔══════════════════════════════════════════════════════════════╗');
140
- _logger.Logger.log(_logger.Logger.INFO_TYPE, '║ Code Coverage Summary ║');
141
- _logger.Logger.log(_logger.Logger.INFO_TYPE, '╚══════════════════════════════════════════════════════════════╝');
142
- const header = ' ' + 'Metric'.padEnd(14) + 'Covered'.padStart(10) + 'Total'.padStart(10) + 'Pct (%)'.padStart(12);
143
- _logger.Logger.log(_logger.Logger.INFO_TYPE, header);
144
- _logger.Logger.log(_logger.Logger.INFO_TYPE, ' ' + '─'.repeat(46));
145
- for (const metric of ['lines', 'statements', 'branches', 'functions']) {
146
- const m = totals[metric];
147
- const icon = m.pct >= 80 ? '✔' : m.pct >= 50 ? '⚠' : '✖';
148
- const line = ` ${icon} ${metric.padEnd(13)}${String(m.covered).padStart(9)}${String(m.total).padStart(10)}${(m.pct + '%').padStart(11)}`;
149
- const type = m.pct >= 80 ? _logger.Logger.SUCCESS_TYPE : _logger.Logger.FAILURE_TYPE;
150
- _logger.Logger.log(type, line);
151
- }
152
- _logger.Logger.log(_logger.Logger.INFO_TYPE, ' ' + '─'.repeat(46));
153
- _logger.Logger.log(_logger.Logger.INFO_TYPE, ` Files analyzed: ${fileSummaries.length}`);
154
- _logger.Logger.log(_logger.Logger.INFO_TYPE, '');
155
- }
156
-
157
- // ── Threshold enforcement ────────────────────────────────────
158
-
159
- _enforceThresholds(totals, thresholds) {
160
- const failures = [];
161
- for (const metric of ['lines', 'statements', 'branches', 'functions']) {
162
- const threshold = thresholds[metric];
163
- if (threshold != null && totals[metric].pct < threshold) {
164
- failures.push(` ✖ ${metric}: ${totals[metric].pct}% < ${threshold}% threshold`);
165
- }
166
- }
167
- if (failures.length > 0) {
168
- _logger.Logger.log(_logger.Logger.FAILURE_TYPE, '\n Coverage thresholds not met:');
169
- for (const f of failures) {
170
- _logger.Logger.log(_logger.Logger.FAILURE_TYPE, f);
171
- }
172
- _logger.Logger.log(_logger.Logger.FAILURE_TYPE, '');
173
- if (this.options.failOnThreshold !== false) {
174
- throw new Error(`Coverage thresholds not met:\n${failures.join('\n')}`);
175
- }
176
- } else {
177
- _logger.Logger.log(_logger.Logger.SUCCESS_TYPE, ' ✔ All coverage thresholds met.\n');
178
- }
179
- }
180
-
181
- // ── HTML Generation ──────────────────────────────────────────
182
-
183
- _generateHtml(totals, fileSummaries, title) {
184
- const timestamp = new Date().toLocaleString();
185
- const summaryCards = ['lines', 'statements', 'branches', 'functions'].map(metric => {
186
- const m = totals[metric];
187
- const cls = m.pct >= 80 ? 'high' : m.pct >= 50 ? 'medium' : 'low';
188
- return `
189
- <div class="card ${cls}">
190
- <div class="value">${m.pct}%</div>
191
- <div class="label">${metric}</div>
192
- <div class="detail">${m.covered} / ${m.total}</div>
193
- </div>`;
194
- }).join('');
195
- const fileRows = fileSummaries.sort((a, b) => a.lines.pct - b.lines.pct) // worst coverage first
196
- .map(f => {
197
- const linesCls = this._pctClass(f.lines.pct);
198
- const branchesCls = this._pctClass(f.branches.pct);
199
- const funcsCls = this._pctClass(f.functions.pct);
200
- const stmtsCls = this._pctClass(f.statements.pct);
201
- return `
202
- <tr>
203
- <td class="file-name">${this._escapeHtml(f.file)}</td>
204
- <td class="${stmtsCls}">${f.statements.pct}%</td>
205
- <td class="${branchesCls}">${f.branches.pct}%</td>
206
- <td class="${funcsCls}">${f.functions.pct}%</td>
207
- <td class="${linesCls}">${f.lines.pct}%</td>
208
- <td>${f.lines.covered}/${f.lines.total}</td>
209
- </tr>`;
210
- }).join('');
211
- return `<!DOCTYPE html>
212
- <html lang="en">
213
- <head>
214
- <meta charset="UTF-8" />
215
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
216
- <title>${title}</title>
217
- <style>
218
- :root {
219
- --high: #2ecc71;
220
- --medium: #f39c12;
221
- --low: #e74c3c;
222
- --bg: #1a1a2e;
223
- --card: #16213e;
224
- --text: #eaeaea;
225
- --muted: #8892a4;
226
- --border: #2a2a4a;
227
- --table-hover: rgba(255,255,255,0.03);
228
- }
229
- * { margin: 0; padding: 0; box-sizing: border-box; }
230
- body {
231
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
232
- background: var(--bg);
233
- color: var(--text);
234
- padding: 24px;
235
- line-height: 1.5;
236
- }
237
- .header { text-align: center; margin-bottom: 32px; }
238
- .header h1 { font-size: 1.8rem; margin-bottom: 4px; }
239
- .header .meta { color: var(--muted); font-size: 0.85rem; }
240
-
241
- /* Summary cards */
242
- .summary {
243
- display: flex; gap: 16px; justify-content: center;
244
- flex-wrap: wrap; margin-bottom: 32px;
245
- }
246
- .card {
247
- background: var(--card); border: 1px solid var(--border);
248
- border-radius: 10px; padding: 18px 28px; min-width: 160px;
249
- text-align: center;
250
- }
251
- .card .value { font-size: 2.2rem; font-weight: 700; }
252
- .card .label {
253
- font-size: 0.8rem; color: var(--muted);
254
- text-transform: uppercase; letter-spacing: 1px;
255
- }
256
- .card .detail { font-size: 0.75rem; color: var(--muted); margin-top: 4px; }
257
- .card.high .value { color: var(--high); }
258
- .card.high { border-color: var(--high); }
259
- .card.medium .value { color: var(--medium); }
260
- .card.medium { border-color: var(--medium); }
261
- .card.low .value { color: var(--low); }
262
- .card.low { border-color: var(--low); }
263
-
264
- /* Coverage bar */
265
- .bar-container {
266
- display: flex; justify-content: center; margin-bottom: 32px;
267
- }
268
- .bar-wrapper { width: 80%; max-width: 700px; }
269
- .bar-label { font-size: 0.85rem; color: var(--muted); margin-bottom: 6px; }
270
- .bar {
271
- height: 22px; background: #2a2a4a; border-radius: 11px; overflow: hidden;
272
- }
273
- .bar-fill {
274
- height: 100%; border-radius: 11px;
275
- transition: width 0.4s ease;
276
- }
277
- .bar-fill.high { background: var(--high); }
278
- .bar-fill.medium { background: var(--medium); }
279
- .bar-fill.low { background: var(--low); }
280
-
281
- /* File table */
282
- .table-container {
283
- background: var(--card); border: 1px solid var(--border);
284
- border-radius: 10px; overflow: hidden; margin-bottom: 24px;
285
- }
286
- .table-title {
287
- padding: 14px 18px; font-weight: 600; font-size: 1rem;
288
- border-bottom: 1px solid var(--border);
289
- }
290
- .search-box {
291
- padding: 10px 18px;
292
- border-bottom: 1px solid var(--border);
293
- }
294
- .search-box input {
295
- width: 100%; padding: 8px 12px;
296
- background: var(--bg); border: 1px solid var(--border);
297
- border-radius: 6px; color: var(--text); font-size: 0.85rem;
298
- }
299
- .search-box input::placeholder { color: var(--muted); }
300
- table {
301
- width: 100%; border-collapse: collapse; font-size: 0.85rem;
302
- }
303
- th {
304
- text-align: left; padding: 10px 14px;
305
- color: var(--muted); font-weight: 600;
306
- text-transform: uppercase; font-size: 0.75rem;
307
- letter-spacing: 0.5px; cursor: pointer;
308
- user-select: none; border-bottom: 1px solid var(--border);
309
- }
310
- th:hover { color: var(--text); }
311
- td {
312
- padding: 10px 14px; border-bottom: 1px solid var(--border);
313
- }
314
- tr:hover { background: var(--table-hover); }
315
- .file-name { font-family: 'SF Mono', Monaco, monospace; font-size: 0.82rem; }
316
- .high { color: var(--high); font-weight: 600; }
317
- .medium { color: var(--medium); font-weight: 600; }
318
- .low { color: var(--low); font-weight: 600; }
319
-
320
- .footer {
321
- text-align: center; color: var(--muted);
322
- font-size: 0.75rem; margin-top: 24px;
323
- }
324
- </style>
325
- </head>
326
- <body>
327
- <div class="header">
328
- <h1>${title}</h1>
329
- <p class="meta">Generated on ${timestamp}</p>
330
- </div>
331
-
332
- <div class="summary">${summaryCards}</div>
333
-
334
- <div class="bar-container">
335
- <div class="bar-wrapper">
336
- <div class="bar-label">Overall Line Coverage: ${totals.lines.pct}%</div>
337
- <div class="bar">
338
- <div class="bar-fill ${this._pctClass(totals.lines.pct)}" style="width: ${totals.lines.pct}%"></div>
339
- </div>
340
- </div>
341
- </div>
342
-
343
- <div class="table-container">
344
- <div class="table-title">Per-File Coverage Breakdown</div>
345
- <div class="search-box">
346
- <input type="text" id="fileFilter" placeholder="Filter files..." oninput="filterFiles()" />
347
- </div>
348
- <table id="coverageTable">
349
- <thead>
350
- <tr>
351
- <th onclick="sortTable(0)">File</th>
352
- <th onclick="sortTable(1)">Stmts %</th>
353
- <th onclick="sortTable(2)">Branch %</th>
354
- <th onclick="sortTable(3)">Funcs %</th>
355
- <th onclick="sortTable(4)">Lines %</th>
356
- <th onclick="sortTable(5)">Lines</th>
357
- </tr>
358
- </thead>
359
- <tbody>${fileRows}</tbody>
360
- </table>
361
- </div>
362
-
363
- <div class="footer">
364
- Generated by Unit Testing Framework &bull; Coverage Reporter
365
- </div>
366
-
367
- <script>
368
- function filterFiles() {
369
- const q = document.getElementById('fileFilter').value.toLowerCase();
370
- const rows = document.querySelectorAll('#coverageTable tbody tr');
371
- rows.forEach(row => {
372
- const name = row.querySelector('.file-name').textContent.toLowerCase();
373
- row.style.display = name.includes(q) ? '' : 'none';
374
- });
375
- }
376
-
377
- let sortDir = {};
378
- function sortTable(col) {
379
- const table = document.getElementById('coverageTable');
380
- const tbody = table.querySelector('tbody');
381
- const rows = Array.from(tbody.querySelectorAll('tr'));
382
- sortDir[col] = !sortDir[col];
383
- rows.sort((a, b) => {
384
- let av = a.cells[col].textContent.replace('%', '').trim();
385
- let bv = b.cells[col].textContent.replace('%', '').trim();
386
- const na = parseFloat(av), nb = parseFloat(bv);
387
- if (!isNaN(na) && !isNaN(nb)) {
388
- return sortDir[col] ? na - nb : nb - na;
389
- }
390
- return sortDir[col] ? av.localeCompare(bv) : bv.localeCompare(av);
391
- });
392
- rows.forEach(r => tbody.appendChild(r));
393
- }
394
- </script>
395
- </body>
396
- </html>`;
397
- }
398
- _pctClass(pct) {
399
- if (pct >= 80) return 'high';
400
- if (pct >= 50) return 'medium';
401
- return 'low';
402
- }
403
- _escapeHtml(str) {
404
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
405
- }
406
- }
407
- exports.default = CoverageReporter;