@zohodesk/unit-testing-framework 0.0.26-experimental → 0.0.28-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/README.md +97 -46
- package/build/src/config/default-config.js +8 -1
- package/build/src/runner/jest-runner.js +1 -1
- package/build/src/utils/logger.js +22 -20
- package/package.json +3 -3
- package/build/src/reporters/default-reporter.js +0 -54
- package/build/src/reporters/html-reporter.js +0 -61
- package/build/src/reporters/html-template.js +0 -177
- package/build/src/reporters/reporter-utils.js +0 -51
- package/build/src/reporters/templates/report-script.js +0 -44
- package/build/src/reporters/templates/report.css +0 -144
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# unit-testing-framework
|
|
1
|
+
# @zohodesk/unit-testing-framework
|
|
2
2
|
|
|
3
|
-
A modular, Jest-based unit testing
|
|
3
|
+
A modular, Jest-based unit testing framework designed to plug into existing CLI pipelines. Runs Jest **programmatically** via `@jest/core` (no shell execution), ships with sensible defaults, HTML report generation via `jest-html-reporter`, and automatic Jest globals injection.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,24 +8,22 @@ A modular, Jest-based unit testing package designed to plug into existing CLI pi
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
unit-testing-framework/
|
|
11
|
-
├── package.json #
|
|
11
|
+
├── package.json # Package config (publishes build/ only)
|
|
12
|
+
├── babel.config.json # Babel config – ESM → CJS for Node 18
|
|
12
13
|
├── index.js # Public API entry point
|
|
13
14
|
├── src/
|
|
14
15
|
│ ├── runner/
|
|
15
|
-
│ │
|
|
16
|
+
│ │ ├── jest-runner.js # createJestRunner() – main orchestrator
|
|
17
|
+
│ │ └── runner-base.js # buildArgv() & executeJest() via @jest/core
|
|
16
18
|
│ ├── config/
|
|
17
|
-
│ │
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
|
|
23
|
-
│ ├── setup.js # Global setup (runs once before suite)
|
|
24
|
-
│ └── teardown.js # Global teardown (runs once after suite)
|
|
19
|
+
│ │ └── default-config.js # getDefaultConfig() – framework defaults
|
|
20
|
+
│ ├── environment/
|
|
21
|
+
│ │ └── globals-inject.js # Injects jest into globalThis (setup file)
|
|
22
|
+
│ └── utils/
|
|
23
|
+
│ └── logger.js # Colored console logger utility
|
|
24
|
+
├── build/ # Transpiled output (published to npm)
|
|
25
25
|
├── examples/
|
|
26
|
-
│
|
|
27
|
-
│ └── jest.unit.config.js # Example consumer config file
|
|
28
|
-
├── .npmignore
|
|
26
|
+
│ └── consumer-cli.js # Example CLI integration
|
|
29
27
|
└── README.md
|
|
30
28
|
```
|
|
31
29
|
|
|
@@ -34,24 +32,38 @@ unit-testing-framework/
|
|
|
34
32
|
## Installation
|
|
35
33
|
|
|
36
34
|
```bash
|
|
37
|
-
npm install unit-testing-framework
|
|
38
|
-
# jest is a required peer dependency
|
|
39
|
-
npm install --save-dev jest
|
|
35
|
+
npm install @zohodesk/unit-testing-framework
|
|
40
36
|
```
|
|
41
37
|
|
|
38
|
+
All Jest dependencies (`@jest/core`, `jest-environment-jsdom`, `babel-jest`) are bundled — consumers don't need to install Jest separately.
|
|
39
|
+
|
|
42
40
|
---
|
|
43
41
|
|
|
44
42
|
## Quick Start
|
|
45
43
|
|
|
46
44
|
```js
|
|
47
|
-
import createJestRunner from 'unit-testing-framework';
|
|
45
|
+
import { createJestRunner } from '@zohodesk/unit-testing-framework';
|
|
48
46
|
|
|
49
|
-
// Run
|
|
47
|
+
// Run all tests with framework defaults
|
|
50
48
|
const results = await createJestRunner();
|
|
51
49
|
|
|
52
50
|
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
53
51
|
```
|
|
54
52
|
|
|
53
|
+
Run a specific test file by pattern:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const results = await createJestRunner({
|
|
57
|
+
testPathPattern: 'login.test.js',
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Enable watch mode:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
await createJestRunner({ watch: true });
|
|
65
|
+
```
|
|
66
|
+
|
|
55
67
|
---
|
|
56
68
|
|
|
57
69
|
## API
|
|
@@ -60,42 +72,81 @@ process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
|
60
72
|
|
|
61
73
|
| Option | Type | Default | Description |
|
|
62
74
|
|---|---|---|---|
|
|
63
|
-
| `projectRoot` | `string` | `process.cwd()` | Consumer project root |
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| `verbose` | `boolean` | `true` | Verbose output |
|
|
69
|
-
| `maxWorkers` | `number\|string` | `'50%'` | Worker concurrency |
|
|
70
|
-
| `silent` | `boolean` | `false` | Suppress console output |
|
|
71
|
-
| `watch` | `boolean` | `false` | Watch mode |
|
|
75
|
+
| `projectRoot` | `string` | `process.cwd()` | Consumer project root directory |
|
|
76
|
+
| `testPathPattern` | `string` | — | Filter tests by file path pattern |
|
|
77
|
+
| `watch` | `boolean` | `false` | Enable Jest watch mode |
|
|
78
|
+
|
|
79
|
+
Returns: `Promise<AggregatedResult>` — Jest's aggregated results object.
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
---
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
## Default Configuration
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
The framework provides a complete Jest config via `getDefaultConfig(projectRoot)`:
|
|
86
|
+
|
|
87
|
+
| Setting | Value |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `testMatch` | `['**/__tests__/**/*.test.js']` |
|
|
90
|
+
| `testEnvironment` | `jsdom` |
|
|
91
|
+
| `transform` | Babel with `@babel/preset-env` |
|
|
92
|
+
| `testTimeout` | 20 000 ms |
|
|
93
|
+
| `automock` | `false` |
|
|
94
|
+
| `clearMocks` | `true` |
|
|
95
|
+
| `passWithNoTests` | `true` |
|
|
96
|
+
| `testPathIgnorePatterns` | `['/build/']` |
|
|
97
|
+
|
|
98
|
+
### Reporters
|
|
99
|
+
|
|
100
|
+
Two reporters are enabled by default:
|
|
101
|
+
|
|
102
|
+
1. **`default`** — Jest's built-in console reporter, prints pass/fail/skip summary to the terminal.
|
|
103
|
+
2. **`jest-html-reporter`** — generates an HTML test report using the [`jest-html-reporter`](https://www.npmjs.com/package/jest-html-reporter) package.
|
|
104
|
+
|
|
105
|
+
#### HTML Reporter Configuration
|
|
106
|
+
|
|
107
|
+
| Option | Value | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `outputPath` | `<rootDir>/test-slices/unit-test/unit_reports/report.html` | Path for the generated report |
|
|
110
|
+
| `pageTitle` | `Unit Test Report` | Page title |
|
|
111
|
+
| `includeFailureMsg` | `true` | Show failure messages in the report |
|
|
112
|
+
| `includeSuiteFailure` | `true` | Show suite-level failures |
|
|
113
|
+
| `theme` | `darkTheme` | Dark-themed report |
|
|
114
|
+
|
|
115
|
+
### Globals Injection
|
|
116
|
+
|
|
117
|
+
The framework's `setupFiles` automatically injects the `jest` object into `globalThis`, so test files can call `jest.fn()`, `jest.mock()`, etc. without explicit imports.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Example: CLI Integration
|
|
122
|
+
|
|
123
|
+
See [examples/consumer-cli.js](examples/consumer-cli.js) for a complete example:
|
|
86
124
|
|
|
87
|
-
Sub-path exports:
|
|
88
125
|
```js
|
|
89
|
-
import {
|
|
90
|
-
|
|
126
|
+
import { createJestRunner } from '@zohodesk/unit-testing-framework';
|
|
127
|
+
|
|
128
|
+
const command = process.argv[2];
|
|
129
|
+
|
|
130
|
+
if (command === 'unit-test') {
|
|
131
|
+
const testFile = process.argv[3];
|
|
132
|
+
const results = await createJestRunner({
|
|
133
|
+
testPathPattern: testFile,
|
|
134
|
+
});
|
|
135
|
+
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
136
|
+
} else if (command === 'unit-test:watch') {
|
|
137
|
+
await createJestRunner({ watch: true });
|
|
138
|
+
}
|
|
91
139
|
```
|
|
92
140
|
|
|
93
141
|
---
|
|
94
142
|
|
|
95
|
-
##
|
|
143
|
+
## Build
|
|
96
144
|
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
Source is written in ESM and transpiled to CommonJS via Babel.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run build # Transpile src/ → build/
|
|
149
|
+
npm run clean # Delete build/ and rebuild
|
|
99
150
|
```
|
|
100
151
|
|
|
101
152
|
### Consumer Config File
|
|
@@ -7,6 +7,7 @@ exports.getDefaultConfig = getDefaultConfig;
|
|
|
7
7
|
var _path = _interopRequireDefault(require("path"));
|
|
8
8
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
function getDefaultConfig(projectRoot) {
|
|
10
|
+
const reportOutputDir = _path.default.resolve(projectRoot, 'test-slices', 'unit-test', 'unit_reports');
|
|
10
11
|
return {
|
|
11
12
|
rootDir: projectRoot,
|
|
12
13
|
testMatch: ['**/__tests__/**/*.test.js'],
|
|
@@ -21,7 +22,13 @@ function getDefaultConfig(projectRoot) {
|
|
|
21
22
|
}]]
|
|
22
23
|
}]
|
|
23
24
|
},
|
|
24
|
-
reporters: ['default',
|
|
25
|
+
reporters: ['default', ['jest-html-reporter', {
|
|
26
|
+
outputPath: _path.default.join(reportOutputDir, 'report.html'),
|
|
27
|
+
pageTitle: 'Unit Test Report',
|
|
28
|
+
includeFailureMsg: true,
|
|
29
|
+
includeSuiteFailure: true,
|
|
30
|
+
theme: 'darkTheme'
|
|
31
|
+
}]],
|
|
25
32
|
testTimeout: 20_000,
|
|
26
33
|
// 20 seconds per test
|
|
27
34
|
|
|
@@ -10,7 +10,7 @@ async function createJestRunner(options = {}) {
|
|
|
10
10
|
const {
|
|
11
11
|
projectRoot = process.cwd(),
|
|
12
12
|
testPathPattern,
|
|
13
|
-
watch = false
|
|
13
|
+
watch = false // Re-runs tests automatically when source files change
|
|
14
14
|
} = options;
|
|
15
15
|
|
|
16
16
|
// ── 1. Build framework-controlled config ───────────────────
|
|
@@ -4,25 +4,27 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.Logger = void 0;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
this.consoleLogger = console;
|
|
18
|
-
}
|
|
19
|
-
error(err) {
|
|
20
|
-
this.consoleLogger.error(err);
|
|
21
|
-
}
|
|
22
|
-
info() {}
|
|
7
|
+
const COLORS = {
|
|
8
|
+
success: '\x1b[36m',
|
|
9
|
+
failure: '\x1b[31m',
|
|
10
|
+
info: '\x1b[33m',
|
|
11
|
+
reset: '\x1b[0m'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
const consoleLogger = console;
|
|
16
|
+
const Logger = exports.Logger = {
|
|
23
17
|
log(type, message) {
|
|
24
|
-
const color =
|
|
25
|
-
|
|
18
|
+
const color = COLORS[type] || COLORS.reset;
|
|
19
|
+
consoleLogger.log(`${color}${message}${COLORS.reset}`);
|
|
20
|
+
},
|
|
21
|
+
error(message) {
|
|
22
|
+
consoleLogger.error(`${COLORS.failure}${message}${COLORS.reset}`);
|
|
23
|
+
},
|
|
24
|
+
info(message) {
|
|
25
|
+
consoleLogger.log(`${COLORS.info}${message}${COLORS.reset}`);
|
|
26
|
+
},
|
|
27
|
+
success(message) {
|
|
28
|
+
consoleLogger.log(`${COLORS.success}${message}${COLORS.reset}`);
|
|
26
29
|
}
|
|
27
|
-
}
|
|
28
|
-
const Logger = exports.Logger = new LoggerImpl();
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zohodesk/unit-testing-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28-experimental",
|
|
4
4
|
"description": "A modular Jest-based unit testing framework",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
14
|
-
"lint": "eslint src/ index.js",
|
|
15
14
|
"build": "babel src -d build/src --copy-files && babel index.js --out-file build/index.js",
|
|
16
15
|
"prepublishOnly": "npm run build",
|
|
17
16
|
"clean": "rm -rf build && npm run build"
|
|
@@ -25,7 +24,8 @@
|
|
|
25
24
|
"@jest/core": "30.2.0",
|
|
26
25
|
"@jest/types": "30.2.0",
|
|
27
26
|
"babel-jest": "30.2.0",
|
|
28
|
-
"jest-environment-jsdom": "30.2.0"
|
|
27
|
+
"jest-environment-jsdom": "30.2.0",
|
|
28
|
+
"jest-html-reporter": "^4.3.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@babel/cli": "7.28.6",
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = void 0;
|
|
7
|
-
var _logger = require("../utils/logger");
|
|
8
|
-
var _reporterUtils = require("./reporter-utils.js");
|
|
9
|
-
/**
|
|
10
|
-
* default-reporter.js
|
|
11
|
-
*
|
|
12
|
-
* A lightweight custom Jest reporter bundled with the framework.
|
|
13
|
-
* Outputs a formatted summary to the console after each test run.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
class DefaultReporter {
|
|
17
|
-
constructor(globalConfig, _reporterOptions) {
|
|
18
|
-
this.globalConfig = globalConfig;
|
|
19
|
-
this._timer = (0, _reporterUtils.createTimer)();
|
|
20
|
-
}
|
|
21
|
-
onRunStart(_results, _options) {
|
|
22
|
-
this._timer.start();
|
|
23
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, '\n╔══════════════════════════════════════════╗');
|
|
24
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, '║ Unit Testing Framework – Test Run ║');
|
|
25
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, '╚══════════════════════════════════════════╝\n');
|
|
26
|
-
}
|
|
27
|
-
onTestStart(test) {
|
|
28
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` ▶ Running: ${test.path}`);
|
|
29
|
-
}
|
|
30
|
-
onTestResult(_test, testResult, _aggregatedResults) {
|
|
31
|
-
const {
|
|
32
|
-
numPassingTests,
|
|
33
|
-
numFailingTests,
|
|
34
|
-
numPendingTests
|
|
35
|
-
} = testResult;
|
|
36
|
-
const icon = numFailingTests > 0 ? '✖' : '✔';
|
|
37
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` ${icon} Passed: ${numPassingTests} Failed: ${numFailingTests} Skipped: ${numPendingTests}`);
|
|
38
|
-
}
|
|
39
|
-
onRunComplete(_contexts, results) {
|
|
40
|
-
const elapsed = this._timer.elapsed();
|
|
41
|
-
const {
|
|
42
|
-
numPassedTests,
|
|
43
|
-
numFailedTests,
|
|
44
|
-
numTotalTests
|
|
45
|
-
} = results;
|
|
46
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, '\n──────────────────────────────────────────');
|
|
47
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` Total: ${numTotalTests}`);
|
|
48
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` Passed: ${numPassedTests}`);
|
|
49
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` Failed: ${numFailedTests}`);
|
|
50
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` Time: ${elapsed}s`);
|
|
51
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, '──────────────────────────────────────────\n');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
exports.default = DefaultReporter;
|
|
@@ -1,61 +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 _reporterUtils = require("./reporter-utils.js");
|
|
10
|
-
var _htmlTemplate = require("./html-template.js");
|
|
11
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
-
/**
|
|
13
|
-
* html-reporter.js
|
|
14
|
-
*
|
|
15
|
-
* Jest reporter that generates a self-contained HTML report.
|
|
16
|
-
* This class handles only the Jest lifecycle hooks.
|
|
17
|
-
* HTML generation is delegated to html-template.js.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
class HtmlReporter {
|
|
21
|
-
/**
|
|
22
|
-
* @param {object} globalConfig - Jest global config
|
|
23
|
-
* @param {object} reporterOptions
|
|
24
|
-
* @param {string} [reporterOptions.outputDir] - Directory for the HTML file (default: <rootDir>/unit_reports)
|
|
25
|
-
* @param {string} [reporterOptions.fileName] - HTML file name (default: report.html)
|
|
26
|
-
* @param {string} [reporterOptions.title] - Page title (default: Unit Test Report)
|
|
27
|
-
*/
|
|
28
|
-
constructor(globalConfig, reporterOptions = {}) {
|
|
29
|
-
this.globalConfig = globalConfig;
|
|
30
|
-
this.options = reporterOptions;
|
|
31
|
-
this._suites = [];
|
|
32
|
-
this._timer = (0, _reporterUtils.createTimer)();
|
|
33
|
-
}
|
|
34
|
-
onRunStart() {
|
|
35
|
-
this._timer.start();
|
|
36
|
-
this._suites = [];
|
|
37
|
-
}
|
|
38
|
-
onTestResult(_test, testResult) {
|
|
39
|
-
this._suites.push(testResult);
|
|
40
|
-
}
|
|
41
|
-
onRunComplete(_contexts, aggregatedResults) {
|
|
42
|
-
const rootDir = this.globalConfig.rootDir || process.cwd();
|
|
43
|
-
const outputDir = this.options.outputDir ? _path.default.resolve(rootDir, this.options.outputDir) : _path.default.resolve(rootDir, 'test-slices', 'unit-test', 'unit_reports');
|
|
44
|
-
const fileName = this.options.fileName || 'report.html';
|
|
45
|
-
const title = this.options.title || 'Unit Test Report';
|
|
46
|
-
const outputPath = _path.default.join(outputDir, fileName);
|
|
47
|
-
_fs.default.mkdirSync(outputDir, {
|
|
48
|
-
recursive: true
|
|
49
|
-
});
|
|
50
|
-
const html = (0, _htmlTemplate.generateHtml)({
|
|
51
|
-
aggregatedResults,
|
|
52
|
-
suites: this._suites,
|
|
53
|
-
rootDir,
|
|
54
|
-
elapsed: this._timer.elapsed(),
|
|
55
|
-
title
|
|
56
|
-
});
|
|
57
|
-
_fs.default.writeFileSync(outputPath, html, 'utf-8');
|
|
58
|
-
console.log(`\n 📄 HTML report written to: ${outputPath}\n`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
exports.default = HtmlReporter;
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.generateHtml = generateHtml;
|
|
7
|
-
exports.renderSuite = renderSuite;
|
|
8
|
-
exports.renderTest = renderTest;
|
|
9
|
-
var _fs = _interopRequireDefault(require("fs"));
|
|
10
|
-
var _path = _interopRequireDefault(require("path"));
|
|
11
|
-
var _reporterUtils = require("./reporter-utils.js");
|
|
12
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
/**
|
|
14
|
-
* html-template.js
|
|
15
|
-
*
|
|
16
|
-
* Pure functions that generate the HTML report string.
|
|
17
|
-
* No file I/O, no Jest API — just data in, HTML string out.
|
|
18
|
-
*
|
|
19
|
-
* CSS and client-side JS are read from separate files under
|
|
20
|
-
* templates/ and inlined at report-generation time so the
|
|
21
|
-
* output remains a single self-contained HTML file.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
// NOTE: __dirname is available after Babel transpiles ESM → CJS.
|
|
25
|
-
|
|
26
|
-
const CSS_PATH = _path.default.resolve(__dirname, 'templates', 'report.css');
|
|
27
|
-
const SCRIPT_PATH = _path.default.resolve(__dirname, 'templates', 'report-script.js');
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Read template assets once and cache them in memory.
|
|
31
|
-
* Lazy-loaded on first call to avoid reading files at import time.
|
|
32
|
-
*/
|
|
33
|
-
let _cssCache = null;
|
|
34
|
-
let _scriptCache = null;
|
|
35
|
-
function getCSS() {
|
|
36
|
-
if (!_cssCache) {
|
|
37
|
-
_cssCache = _fs.default.readFileSync(CSS_PATH, 'utf-8');
|
|
38
|
-
}
|
|
39
|
-
return _cssCache;
|
|
40
|
-
}
|
|
41
|
-
function getScript() {
|
|
42
|
-
if (!_scriptCache) {
|
|
43
|
-
_scriptCache = _fs.default.readFileSync(SCRIPT_PATH, 'utf-8');
|
|
44
|
-
}
|
|
45
|
-
return _scriptCache;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generate the full HTML report string.
|
|
50
|
-
*
|
|
51
|
-
* @param {object} params
|
|
52
|
-
* @param {object} params.aggregatedResults - Jest aggregated results.
|
|
53
|
-
* @param {object[]} params.suites - Collected test suite results.
|
|
54
|
-
* @param {string} params.rootDir - Project root for relative paths.
|
|
55
|
-
* @param {string} params.elapsed - Elapsed time string (e.g. "1.23").
|
|
56
|
-
* @param {string} params.title - Report page title.
|
|
57
|
-
* @returns {string} Self-contained HTML string.
|
|
58
|
-
*/
|
|
59
|
-
function generateHtml({
|
|
60
|
-
aggregatedResults,
|
|
61
|
-
suites,
|
|
62
|
-
rootDir,
|
|
63
|
-
elapsed,
|
|
64
|
-
title
|
|
65
|
-
}) {
|
|
66
|
-
const {
|
|
67
|
-
numPassedTests,
|
|
68
|
-
numFailedTests,
|
|
69
|
-
numPendingTests,
|
|
70
|
-
numTotalTests,
|
|
71
|
-
numTotalTestSuites
|
|
72
|
-
} = aggregatedResults;
|
|
73
|
-
const suitesHtml = suites.map(suite => renderSuite(suite, rootDir)).join('\n');
|
|
74
|
-
const timestamp = new Date().toLocaleString();
|
|
75
|
-
const css = getCSS();
|
|
76
|
-
const script = getScript();
|
|
77
|
-
return `<!DOCTYPE html>
|
|
78
|
-
<html lang="en">
|
|
79
|
-
<head>
|
|
80
|
-
<meta charset="UTF-8" />
|
|
81
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
82
|
-
<title>${(0, _reporterUtils.escapeHtml)(title)}</title>
|
|
83
|
-
<style>
|
|
84
|
-
${css}
|
|
85
|
-
</style>
|
|
86
|
-
</head>
|
|
87
|
-
<body>
|
|
88
|
-
<div class="header">
|
|
89
|
-
<h1>${(0, _reporterUtils.escapeHtml)(title)}</h1>
|
|
90
|
-
<div class="meta">${timestamp} · Duration: ${elapsed}s</div>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div class="summary">
|
|
94
|
-
<div class="card total"><div class="value">${numTotalTests}</div><div class="label">Total</div></div>
|
|
95
|
-
<div class="card pass"><div class="value">${numPassedTests}</div><div class="label">Passed</div></div>
|
|
96
|
-
<div class="card fail"><div class="value">${numFailedTests}</div><div class="label">Failed</div></div>
|
|
97
|
-
<div class="card skip"><div class="value">${numPendingTests}</div><div class="label">Skipped</div></div>
|
|
98
|
-
<div class="card"><div class="value">${numTotalTestSuites}</div><div class="label">Suites</div></div>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<div class="filters">
|
|
102
|
-
<button class="active" data-filter="all">All</button>
|
|
103
|
-
<button data-filter="passed">Passed</button>
|
|
104
|
-
<button data-filter="failed">Failed</button>
|
|
105
|
-
<button data-filter="pending">Skipped</button>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div id="suites">
|
|
109
|
-
${suitesHtml}
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div class="footer">
|
|
113
|
-
Generated by @zohodesk/unit-testing-framework
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<script>
|
|
117
|
-
${script}
|
|
118
|
-
</script>
|
|
119
|
-
</body>
|
|
120
|
-
</html>`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Render a single test suite section.
|
|
125
|
-
*
|
|
126
|
-
* @param {object} suite - Jest test suite result.
|
|
127
|
-
* @param {string} rootDir - Project root for computing relative paths.
|
|
128
|
-
* @returns {string} HTML string for the suite.
|
|
129
|
-
*/
|
|
130
|
-
function renderSuite(suite, rootDir) {
|
|
131
|
-
const suitePath = suite.testFilePath || 'Unknown suite';
|
|
132
|
-
const relativePath = _path.default.relative(rootDir, suitePath);
|
|
133
|
-
const passed = suite.numPassingTests || 0;
|
|
134
|
-
const failed = suite.numFailingTests || 0;
|
|
135
|
-
const pending = suite.numPendingTests || 0;
|
|
136
|
-
const suiteIcon = failed > 0 ? '✖' : pending > 0 && passed === 0 ? '○' : '✔';
|
|
137
|
-
const suiteIconColor = failed > 0 ? 'var(--fail)' : 'var(--pass)';
|
|
138
|
-
const testsHtml = (suite.testResults || []).map(t => renderTest(t)).join('\n');
|
|
139
|
-
return `
|
|
140
|
-
<div class="suite">
|
|
141
|
-
<div class="suite-header">
|
|
142
|
-
<span class="icon" style="color:${suiteIconColor}">${suiteIcon}</span>
|
|
143
|
-
<span class="name">${(0, _reporterUtils.escapeHtml)(relativePath)}</span>
|
|
144
|
-
<span class="counts">${passed} passed · ${failed} failed · ${pending} skipped</span>
|
|
145
|
-
<span class="arrow">▶</span>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="suite-body">
|
|
148
|
-
${testsHtml}
|
|
149
|
-
</div>
|
|
150
|
-
</div>`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Render a single test row.
|
|
155
|
-
*
|
|
156
|
-
* @param {object} testResult - Individual test result from Jest.
|
|
157
|
-
* @returns {string} HTML string for the test row.
|
|
158
|
-
*/
|
|
159
|
-
function renderTest(testResult) {
|
|
160
|
-
const status = testResult.status; // 'passed' | 'failed' | 'pending'
|
|
161
|
-
const icon = status === 'passed' ? '✔' : status === 'failed' ? '✖' : '○';
|
|
162
|
-
const duration = testResult.duration != null ? `${testResult.duration}ms` : '';
|
|
163
|
-
const title = testResult.ancestorTitles ? [...testResult.ancestorTitles, testResult.title].join(' › ') : testResult.title;
|
|
164
|
-
let failureHtml = '';
|
|
165
|
-
if (status === 'failed' && testResult.failureMessages?.length) {
|
|
166
|
-
failureHtml = testResult.failureMessages.map(msg => `<div class="failure-msg">${(0, _reporterUtils.escapeHtml)((0, _reporterUtils.stripAnsi)(msg))}</div>`).join('\n');
|
|
167
|
-
}
|
|
168
|
-
return `
|
|
169
|
-
<div class="test-row ${status}">
|
|
170
|
-
<span class="status">${icon}</span>
|
|
171
|
-
<div class="test-title">
|
|
172
|
-
${(0, _reporterUtils.escapeHtml)(title)}
|
|
173
|
-
${failureHtml}
|
|
174
|
-
</div>
|
|
175
|
-
<span class="duration">${duration}</span>
|
|
176
|
-
</div>`;
|
|
177
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.createTimer = createTimer;
|
|
7
|
-
exports.escapeHtml = escapeHtml;
|
|
8
|
-
exports.stripAnsi = stripAnsi;
|
|
9
|
-
/**
|
|
10
|
-
* reporter-utils.js
|
|
11
|
-
*
|
|
12
|
-
* Shared utilities used by both the HTML and default reporters.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Create a simple timer that tracks elapsed seconds.
|
|
17
|
-
*
|
|
18
|
-
* @returns {{ start(): void, elapsed(): string }}
|
|
19
|
-
*/
|
|
20
|
-
function createTimer() {
|
|
21
|
-
let startTime = 0;
|
|
22
|
-
return {
|
|
23
|
-
start() {
|
|
24
|
-
startTime = Date.now();
|
|
25
|
-
},
|
|
26
|
-
elapsed() {
|
|
27
|
-
return ((Date.now() - startTime) / 1000).toFixed(2);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Escape HTML special characters to prevent injection.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} str
|
|
36
|
-
* @returns {string}
|
|
37
|
-
*/
|
|
38
|
-
function escapeHtml(str) {
|
|
39
|
-
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Strip ANSI escape codes from a string.
|
|
44
|
-
*
|
|
45
|
-
* @param {string} str
|
|
46
|
-
* @returns {string}
|
|
47
|
-
*/
|
|
48
|
-
function stripAnsi(str) {
|
|
49
|
-
// eslint-disable-next-line no-control-regex
|
|
50
|
-
return String(str).replace(/\x1B\[[0-9;]*m/g, '');
|
|
51
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Toggle suite expand/collapse
|
|
2
|
-
document.querySelectorAll('.suite-header').forEach(function (header) {
|
|
3
|
-
header.addEventListener('click', function () {
|
|
4
|
-
header.parentElement.classList.toggle('open');
|
|
5
|
-
});
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// Filter buttons
|
|
9
|
-
document.querySelectorAll('.filters button').forEach(function (btn) {
|
|
10
|
-
btn.addEventListener('click', function () {
|
|
11
|
-
document.querySelectorAll('.filters button').forEach(function (b) {
|
|
12
|
-
b.classList.remove('active');
|
|
13
|
-
});
|
|
14
|
-
btn.classList.add('active');
|
|
15
|
-
var filter = btn.dataset.filter;
|
|
16
|
-
document.querySelectorAll('.suite').forEach(function (suite) {
|
|
17
|
-
if (filter === 'all') {
|
|
18
|
-
suite.style.display = '';
|
|
19
|
-
suite.querySelectorAll('.test-row').forEach(function (r) {
|
|
20
|
-
r.style.display = '';
|
|
21
|
-
});
|
|
22
|
-
} else {
|
|
23
|
-
var rows = suite.querySelectorAll('.test-row');
|
|
24
|
-
var visible = 0;
|
|
25
|
-
rows.forEach(function (r) {
|
|
26
|
-
if (r.classList.contains(filter)) {
|
|
27
|
-
r.style.display = '';
|
|
28
|
-
visible++;
|
|
29
|
-
} else {
|
|
30
|
-
r.style.display = 'none';
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
suite.style.display = visible > 0 ? '' : 'none';
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Auto-expand suites with failures
|
|
40
|
-
document.querySelectorAll('.suite').forEach(function (suite) {
|
|
41
|
-
if (suite.querySelector('.test-row.failed')) {
|
|
42
|
-
suite.classList.add('open');
|
|
43
|
-
}
|
|
44
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--pass: #2ecc71;
|
|
3
|
-
--fail: #e74c3c;
|
|
4
|
-
--skip: #f39c12;
|
|
5
|
-
--bg: #1a1a2e;
|
|
6
|
-
--card: #16213e;
|
|
7
|
-
--text: #eaeaea;
|
|
8
|
-
--muted: #8892a4;
|
|
9
|
-
--border: #2a2a4a;
|
|
10
|
-
}
|
|
11
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
-
body {
|
|
13
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
|
|
14
|
-
background: var(--bg);
|
|
15
|
-
color: var(--text);
|
|
16
|
-
padding: 24px;
|
|
17
|
-
line-height: 1.5;
|
|
18
|
-
}
|
|
19
|
-
.header {
|
|
20
|
-
text-align: center;
|
|
21
|
-
margin-bottom: 32px;
|
|
22
|
-
}
|
|
23
|
-
.header h1 { font-size: 1.8rem; margin-bottom: 4px; }
|
|
24
|
-
.header .meta { color: var(--muted); font-size: 0.85rem; }
|
|
25
|
-
|
|
26
|
-
/* ── Summary Cards ─── */
|
|
27
|
-
.summary {
|
|
28
|
-
display: flex;
|
|
29
|
-
gap: 16px;
|
|
30
|
-
justify-content: center;
|
|
31
|
-
flex-wrap: wrap;
|
|
32
|
-
margin-bottom: 28px;
|
|
33
|
-
}
|
|
34
|
-
.card {
|
|
35
|
-
background: var(--card);
|
|
36
|
-
border: 1px solid var(--border);
|
|
37
|
-
border-radius: 10px;
|
|
38
|
-
padding: 18px 28px;
|
|
39
|
-
min-width: 140px;
|
|
40
|
-
text-align: center;
|
|
41
|
-
}
|
|
42
|
-
.card .value { font-size: 2rem; font-weight: 700; }
|
|
43
|
-
.card .label { font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
|
|
44
|
-
.card.pass .value { color: var(--pass); }
|
|
45
|
-
.card.fail .value { color: var(--fail); }
|
|
46
|
-
.card.skip .value { color: var(--skip); }
|
|
47
|
-
.card.total .value { color: #58a6ff; }
|
|
48
|
-
|
|
49
|
-
/* ── Filters ─── */
|
|
50
|
-
.filters {
|
|
51
|
-
display: flex;
|
|
52
|
-
gap: 8px;
|
|
53
|
-
justify-content: center;
|
|
54
|
-
margin-bottom: 24px;
|
|
55
|
-
flex-wrap: wrap;
|
|
56
|
-
}
|
|
57
|
-
.filters button {
|
|
58
|
-
background: var(--card);
|
|
59
|
-
color: var(--text);
|
|
60
|
-
border: 1px solid var(--border);
|
|
61
|
-
padding: 6px 18px;
|
|
62
|
-
border-radius: 20px;
|
|
63
|
-
cursor: pointer;
|
|
64
|
-
font-size: 0.85rem;
|
|
65
|
-
transition: all 0.2s;
|
|
66
|
-
}
|
|
67
|
-
.filters button:hover,
|
|
68
|
-
.filters button.active { background: #58a6ff; color: #000; border-color: #58a6ff; }
|
|
69
|
-
|
|
70
|
-
/* ── Suites ─── */
|
|
71
|
-
.suite {
|
|
72
|
-
background: var(--card);
|
|
73
|
-
border: 1px solid var(--border);
|
|
74
|
-
border-radius: 10px;
|
|
75
|
-
margin-bottom: 16px;
|
|
76
|
-
overflow: hidden;
|
|
77
|
-
}
|
|
78
|
-
.suite-header {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
padding: 14px 18px;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
user-select: none;
|
|
84
|
-
gap: 10px;
|
|
85
|
-
}
|
|
86
|
-
.suite-header:hover { background: rgba(255,255,255,0.03); }
|
|
87
|
-
.suite-header .icon { font-size: 1.1rem; }
|
|
88
|
-
.suite-header .name {
|
|
89
|
-
flex: 1;
|
|
90
|
-
font-weight: 600;
|
|
91
|
-
font-size: 0.95rem;
|
|
92
|
-
word-break: break-all;
|
|
93
|
-
}
|
|
94
|
-
.suite-header .counts { font-size: 0.8rem; color: var(--muted); }
|
|
95
|
-
.suite-header .arrow {
|
|
96
|
-
transition: transform 0.2s;
|
|
97
|
-
color: var(--muted);
|
|
98
|
-
}
|
|
99
|
-
.suite.open .suite-header .arrow { transform: rotate(90deg); }
|
|
100
|
-
|
|
101
|
-
.suite-body { display: none; border-top: 1px solid var(--border); }
|
|
102
|
-
.suite.open .suite-body { display: block; }
|
|
103
|
-
|
|
104
|
-
.test-row {
|
|
105
|
-
display: flex;
|
|
106
|
-
align-items: flex-start;
|
|
107
|
-
padding: 10px 18px 10px 36px;
|
|
108
|
-
gap: 10px;
|
|
109
|
-
border-bottom: 1px solid var(--border);
|
|
110
|
-
font-size: 0.9rem;
|
|
111
|
-
}
|
|
112
|
-
.test-row:last-child { border-bottom: none; }
|
|
113
|
-
.test-row .status {
|
|
114
|
-
flex-shrink: 0;
|
|
115
|
-
width: 20px;
|
|
116
|
-
text-align: center;
|
|
117
|
-
}
|
|
118
|
-
.test-row .test-title { flex: 1; }
|
|
119
|
-
.test-row .duration { color: var(--muted); font-size: 0.8rem; flex-shrink: 0; }
|
|
120
|
-
.test-row.passed .status { color: var(--pass); }
|
|
121
|
-
.test-row.failed .status { color: var(--fail); }
|
|
122
|
-
.test-row.pending .status { color: var(--skip); }
|
|
123
|
-
|
|
124
|
-
.failure-msg {
|
|
125
|
-
background: rgba(231, 76, 60, 0.1);
|
|
126
|
-
border-left: 3px solid var(--fail);
|
|
127
|
-
margin: 6px 0 4px 30px;
|
|
128
|
-
padding: 10px 14px;
|
|
129
|
-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
130
|
-
font-size: 0.8rem;
|
|
131
|
-
white-space: pre-wrap;
|
|
132
|
-
word-break: break-word;
|
|
133
|
-
color: #f5a5a5;
|
|
134
|
-
border-radius: 4px;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.footer {
|
|
138
|
-
text-align: center;
|
|
139
|
-
color: var(--muted);
|
|
140
|
-
font-size: 0.75rem;
|
|
141
|
-
margin-top: 32px;
|
|
142
|
-
padding-top: 16px;
|
|
143
|
-
border-top: 1px solid var(--border);
|
|
144
|
-
}
|