@zohodesk/unit-testing-framework 0.0.14-experimental → 0.0.15-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 +33 -0
- package/build/src/config/default-config.js +10 -6
- package/build/src/constants/configConstants.js +21 -0
- package/build/src/coverage/coverage-config.js +77 -0
- package/build/src/coverage/coverage-manager.js +88 -0
- package/build/src/coverage/coverage-runner.js +123 -0
- package/build/src/reporters/coverage-reporter.js +407 -0
- package/build/src/reporters/reporter-handler.js +2 -1
- package/build/src/runner/jest-runner.js +14 -78
- package/build/src/runner/runner-base.js +112 -0
- package/package.json +3 -2
package/build/index.js
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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
|
+
});
|
|
6
24
|
Object.defineProperty(exports, "createJestRunner", {
|
|
7
25
|
enumerable: true,
|
|
8
26
|
get: function () {
|
|
@@ -15,5 +33,20 @@ Object.defineProperty(exports, "default", {
|
|
|
15
33
|
return _jestRunner.default;
|
|
16
34
|
}
|
|
17
35
|
});
|
|
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
|
+
});
|
|
18
48
|
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");
|
|
19
52
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -35,7 +35,7 @@ function getDefaultConfig(projectRoot) {
|
|
|
35
35
|
testMatch: ['**/__tests__/**/*.test.js'],
|
|
36
36
|
testPathIgnorePatterns: ['/node_modules/', '/build/'],
|
|
37
37
|
// --------------- Environment ---------------
|
|
38
|
-
testEnvironment: '
|
|
38
|
+
testEnvironment: 'jsdom',
|
|
39
39
|
// --------------- Transform ---------------
|
|
40
40
|
// Use babel-jest to transform ESM (import/export) and JSX in
|
|
41
41
|
// consumer source & test files. Consumer's own babel config
|
|
@@ -52,12 +52,16 @@ function getDefaultConfig(projectRoot) {
|
|
|
52
52
|
},
|
|
53
53
|
transformIgnorePatterns: ['/node_modules/'],
|
|
54
54
|
// --------------- Coverage ---------------
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
|
|
59
60
|
// --------------- Reporters ---------------
|
|
60
|
-
reporters: ['default', 'html-report'
|
|
61
|
+
reporters: ['default', 'html-report'
|
|
62
|
+
// 'coverage-report' is injected automatically by
|
|
63
|
+
// CoverageManager when coverage is enabled.
|
|
64
|
+
],
|
|
61
65
|
// --------------- Parallelism & Performance ---------------
|
|
62
66
|
maxWorkers: '50%',
|
|
63
67
|
// Use half of available CPUs
|
|
@@ -5,5 +5,26 @@ 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';
|
|
8
29
|
}
|
|
9
30
|
module.exports = configConstants;
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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;
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
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 • 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
exports.default = CoverageReporter;
|
|
@@ -23,7 +23,8 @@ 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')
|
|
26
|
+
'html-report': _path.default.resolve(__dirname, 'html-reporter.js'),
|
|
27
|
+
'coverage-report': _path.default.resolve(__dirname, 'coverage-reporter.js')
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -4,11 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = createJestRunner;
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
var _reporterHandler = require("../reporters/reporter-handler.js");
|
|
10
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
-
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); } /**
|
|
7
|
+
var _runnerBase = require("./runner-base.js");
|
|
8
|
+
/**
|
|
12
9
|
* jest-runner.js
|
|
13
10
|
*
|
|
14
11
|
* Core module that runs Jest programmatically via @jest/core's runCLI.
|
|
@@ -17,10 +14,10 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
17
14
|
* import createJestRunner from 'unit-testing-framework';
|
|
18
15
|
* await createJestRunner({ projectRoot: process.cwd() });
|
|
19
16
|
*/
|
|
17
|
+
|
|
20
18
|
/**
|
|
21
19
|
* @typedef {object} RunnerOptions
|
|
22
20
|
* @property {string} [projectRoot] - Absolute path to the consumer project (default: cwd).
|
|
23
|
-
* @property {boolean} [coverage] - Enable coverage collection.
|
|
24
21
|
* @property {string[]} [testFiles] - Specific test file patterns to run.
|
|
25
22
|
* @property {string} [testPathPattern] - Regex pattern to match test file paths (e.g. 'myFile.test.js').
|
|
26
23
|
* @property {boolean} [verbose] - Verbose output.
|
|
@@ -38,7 +35,6 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
38
35
|
async function createJestRunner(options = {}) {
|
|
39
36
|
const {
|
|
40
37
|
projectRoot = process.cwd(),
|
|
41
|
-
coverage,
|
|
42
38
|
testFiles,
|
|
43
39
|
testPathPattern,
|
|
44
40
|
verbose,
|
|
@@ -47,82 +43,22 @@ async function createJestRunner(options = {}) {
|
|
|
47
43
|
watch = false
|
|
48
44
|
} = options;
|
|
49
45
|
|
|
50
|
-
// ── 1.
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (maxWorkers !== undefined) mergedConfig.maxWorkers = maxWorkers;
|
|
57
|
-
if (silent !== undefined) mergedConfig.silent = silent;
|
|
46
|
+
// ── 1. Build base config with overrides ────────────────────
|
|
47
|
+
const config = (0, _runnerBase.createBaseConfig)(projectRoot, {
|
|
48
|
+
verbose,
|
|
49
|
+
maxWorkers,
|
|
50
|
+
silent
|
|
51
|
+
});
|
|
58
52
|
|
|
59
|
-
// ──
|
|
60
|
-
|
|
53
|
+
// ── 2. Resolve reporters ───────────────────────────────────
|
|
54
|
+
(0, _runnerBase.resolveConfigReporters)(config, projectRoot);
|
|
61
55
|
|
|
62
|
-
// ──
|
|
63
|
-
|
|
64
|
-
const argv = buildArgv(mergedConfig, {
|
|
56
|
+
// ── 3. Build argv & run Jest ───────────────────────────────
|
|
57
|
+
const argv = (0, _runnerBase.buildArgv)(config, {
|
|
65
58
|
testFiles,
|
|
66
59
|
testPathPattern,
|
|
67
60
|
watch,
|
|
68
61
|
projectRoot
|
|
69
62
|
});
|
|
70
|
-
|
|
71
|
-
// ── 5. Run Jest programmatically ───────────────────────────
|
|
72
|
-
// Lazy-import to keep startup fast; @jest/core is heavy.
|
|
73
|
-
const {
|
|
74
|
-
runCLI
|
|
75
|
-
} = await Promise.resolve().then(() => _interopRequireWildcard(require('@jest/core')));
|
|
76
|
-
const {
|
|
77
|
-
results
|
|
78
|
-
} = await runCLI(argv, [projectRoot]);
|
|
79
|
-
|
|
80
|
-
// ── 6. Return results for the caller to inspect ────────────
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Build a yargs-compatible argv object that runCLI understands.
|
|
86
|
-
*
|
|
87
|
-
* @param {object} config - Merged Jest config.
|
|
88
|
-
* @param {object} extra
|
|
89
|
-
* @param {string[]} [extra.testFiles]
|
|
90
|
-
* @param {string} [extra.testPathPattern]
|
|
91
|
-
* @param {boolean} [extra.watch]
|
|
92
|
-
* @param {string} extra.projectRoot
|
|
93
|
-
* @returns {object}
|
|
94
|
-
*/
|
|
95
|
-
function buildArgv(config, {
|
|
96
|
-
testFiles,
|
|
97
|
-
testPathPattern,
|
|
98
|
-
watch,
|
|
99
|
-
projectRoot
|
|
100
|
-
}) {
|
|
101
|
-
const argv = {
|
|
102
|
-
// Serialise the config so Jest uses our merged config
|
|
103
|
-
// instead of searching for jest.config.* files.
|
|
104
|
-
config: JSON.stringify(config),
|
|
105
|
-
// Project root for resolution
|
|
106
|
-
rootDir: projectRoot,
|
|
107
|
-
// Flags
|
|
108
|
-
watch: watch,
|
|
109
|
-
watchAll: false,
|
|
110
|
-
ci: process.env.CI === 'true',
|
|
111
|
-
// Pass-through values that CLI users might expect
|
|
112
|
-
verbose: config.verbose ?? true,
|
|
113
|
-
collectCoverage: config.collectCoverage ?? false,
|
|
114
|
-
passWithNoTests: config.passWithNoTests ?? true,
|
|
115
|
-
maxWorkers: config.maxWorkers ?? '50%',
|
|
116
|
-
silent: config.silent ?? false,
|
|
117
|
-
// Do not search for config files automatically
|
|
118
|
-
_: testFiles ?? [],
|
|
119
|
-
$0: 'unit-testing-framework'
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// If a specific test file/pattern is provided, use testPathPattern
|
|
123
|
-
// so Jest only runs matching test files.
|
|
124
|
-
if (testPathPattern) {
|
|
125
|
-
argv.testPathPattern = testPathPattern;
|
|
126
|
-
}
|
|
127
|
-
return argv;
|
|
63
|
+
return (0, _runnerBase.executeJest)(argv, projectRoot);
|
|
128
64
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.buildArgv = buildArgv;
|
|
7
|
+
exports.createBaseConfig = createBaseConfig;
|
|
8
|
+
exports.executeJest = executeJest;
|
|
9
|
+
exports.resolveConfigReporters = resolveConfigReporters;
|
|
10
|
+
var _defaultConfig = require("../config/default-config.js");
|
|
11
|
+
var _reporterHandler = require("../reporters/reporter-handler.js");
|
|
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
|
+
* runner-base.js
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Create the base Jest config with common CLI overrides applied.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} projectRoot
|
|
24
|
+
* @param {object} [overrides]
|
|
25
|
+
* @param {boolean} [overrides.verbose]
|
|
26
|
+
* @param {number|string} [overrides.maxWorkers]
|
|
27
|
+
* @param {boolean} [overrides.silent]
|
|
28
|
+
* @returns {import('@jest/types').Config.InitialOptions}
|
|
29
|
+
*/
|
|
30
|
+
function createBaseConfig(projectRoot, overrides = {}) {
|
|
31
|
+
const config = (0, _defaultConfig.getDefaultConfig)(projectRoot);
|
|
32
|
+
if (overrides.verbose !== undefined) config.verbose = overrides.verbose;
|
|
33
|
+
if (overrides.maxWorkers !== undefined) config.maxWorkers = overrides.maxWorkers;
|
|
34
|
+
if (overrides.silent !== undefined) config.silent = overrides.silent;
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve reporter aliases in the config and return the updated config.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} config - Mutable Jest config.
|
|
42
|
+
* @param {string} projectRoot
|
|
43
|
+
* @returns {object} The same config reference (mutated).
|
|
44
|
+
*/
|
|
45
|
+
function resolveConfigReporters(config, projectRoot) {
|
|
46
|
+
config.reporters = (0, _reporterHandler.resolveReporters)(config.reporters, projectRoot);
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a yargs-compatible argv object that runCLI understands.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} config - Merged Jest config.
|
|
54
|
+
* @param {object} params
|
|
55
|
+
* @param {string[]} [params.testFiles] - Explicit test file patterns.
|
|
56
|
+
* @param {string} [params.testPathPattern] - Regex to filter test files.
|
|
57
|
+
* @param {boolean} [params.watch] - Watch mode flag.
|
|
58
|
+
* @param {string} params.projectRoot - Consumer project root.
|
|
59
|
+
* @param {object} [params.argvOverrides] - Extra keys merged into argv (e.g. { collectCoverage: true }).
|
|
60
|
+
* @returns {object}
|
|
61
|
+
*/
|
|
62
|
+
function buildArgv(config, {
|
|
63
|
+
testFiles,
|
|
64
|
+
testPathPattern,
|
|
65
|
+
watch = false,
|
|
66
|
+
projectRoot,
|
|
67
|
+
argvOverrides = {}
|
|
68
|
+
}) {
|
|
69
|
+
const argv = {
|
|
70
|
+
// Serialise the config so Jest uses our merged config
|
|
71
|
+
// instead of searching for jest.config.* files.
|
|
72
|
+
config: JSON.stringify(config),
|
|
73
|
+
// Project root for resolution
|
|
74
|
+
rootDir: projectRoot,
|
|
75
|
+
// Flags
|
|
76
|
+
watch,
|
|
77
|
+
watchAll: false,
|
|
78
|
+
ci: process.env.CI === 'true',
|
|
79
|
+
// Pass-through values that CLI users might expect
|
|
80
|
+
verbose: config.verbose ?? true,
|
|
81
|
+
collectCoverage: config.collectCoverage ?? false,
|
|
82
|
+
passWithNoTests: config.passWithNoTests ?? true,
|
|
83
|
+
maxWorkers: config.maxWorkers ?? '50%',
|
|
84
|
+
silent: config.silent ?? false,
|
|
85
|
+
// Do not search for config files automatically
|
|
86
|
+
_: testFiles ?? [],
|
|
87
|
+
$0: 'unit-testing-framework',
|
|
88
|
+
// Caller-specific overrides (e.g. coverage runner forces collectCoverage: true)
|
|
89
|
+
...argvOverrides
|
|
90
|
+
};
|
|
91
|
+
if (testPathPattern) {
|
|
92
|
+
argv.testPathPattern = testPathPattern;
|
|
93
|
+
}
|
|
94
|
+
return argv;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lazy-import @jest/core and execute runCLI.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} argv - yargs-style argv for runCLI.
|
|
101
|
+
* @param {string} projectRoot
|
|
102
|
+
* @returns {Promise<import('@jest/core').AggregatedResult>}
|
|
103
|
+
*/
|
|
104
|
+
async function executeJest(argv, projectRoot) {
|
|
105
|
+
const {
|
|
106
|
+
runCLI
|
|
107
|
+
} = await Promise.resolve().then(() => _interopRequireWildcard(require('@jest/core')));
|
|
108
|
+
const {
|
|
109
|
+
results
|
|
110
|
+
} = await runCLI(argv, [projectRoot]);
|
|
111
|
+
return results;
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zohodesk/unit-testing-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15-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"
|
|
10
|
+
"./runner": "./build/src/runner/jest-runner.js",
|
|
11
|
+
"./coverage": "./build/src/coverage/coverage-runner.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"build/"
|