@zohodesk/unit-testing-framework 0.0.26-experimental → 0.0.27-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 +104 -45
- package/build/src/config/default-config.js +8 -1
- package/package.json +3 -2
- 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, a console reporter, an interactive HTML reporter, and automatic Jest globals injection.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,24 +8,30 @@ 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
|
-
│ │ └── default-config.js # Framework-level default Jest config
|
|
19
|
+
│ │ └── default-config.js # getDefaultConfig() – framework defaults
|
|
19
20
|
│ ├── reporters/
|
|
20
|
-
│ │ ├── reporter
|
|
21
|
-
│ │
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
│
|
|
21
|
+
│ │ ├── default-reporter.js # Console reporter (pass/fail/skip summary)
|
|
22
|
+
│ │ ├── html-reporter.js # Self-contained HTML report generator
|
|
23
|
+
│ │ ├── html-template.js # HTML markup generation functions
|
|
24
|
+
│ │ ├── reporter-utils.js # Shared utils (timer, escapeHtml, stripAnsi)
|
|
25
|
+
│ │ └── templates/
|
|
26
|
+
│ │ ├── report-script.js # Client-side JS (filters, collapsible suites)
|
|
27
|
+
│ │ └── report.css # Dark-themed report stylesheet
|
|
28
|
+
│ ├── environment/
|
|
29
|
+
│ │ └── globals-inject.js # Injects jest into globalThis (setup file)
|
|
30
|
+
│ └── utils/
|
|
31
|
+
│ └── logger.js # Colored console logger singleton
|
|
32
|
+
├── build/ # Transpiled output (published to npm)
|
|
25
33
|
├── examples/
|
|
26
|
-
│
|
|
27
|
-
│ └── jest.unit.config.js # Example consumer config file
|
|
28
|
-
├── .npmignore
|
|
34
|
+
│ └── consumer-cli.js # Example CLI integration
|
|
29
35
|
└── README.md
|
|
30
36
|
```
|
|
31
37
|
|
|
@@ -34,24 +40,38 @@ unit-testing-framework/
|
|
|
34
40
|
## Installation
|
|
35
41
|
|
|
36
42
|
```bash
|
|
37
|
-
npm install unit-testing-framework
|
|
38
|
-
# jest is a required peer dependency
|
|
39
|
-
npm install --save-dev jest
|
|
43
|
+
npm install @zohodesk/unit-testing-framework
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
All Jest dependencies (`@jest/core`, `jest-environment-jsdom`, `babel-jest`) are bundled — consumers don't need to install Jest separately.
|
|
47
|
+
|
|
42
48
|
---
|
|
43
49
|
|
|
44
50
|
## Quick Start
|
|
45
51
|
|
|
46
52
|
```js
|
|
47
|
-
import createJestRunner from 'unit-testing-framework';
|
|
53
|
+
import { createJestRunner } from '@zohodesk/unit-testing-framework';
|
|
48
54
|
|
|
49
|
-
// Run
|
|
55
|
+
// Run all tests with framework defaults
|
|
50
56
|
const results = await createJestRunner();
|
|
51
57
|
|
|
52
58
|
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
Run a specific test file by pattern:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const results = await createJestRunner({
|
|
65
|
+
testPathPattern: 'login.test.js',
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Enable watch mode:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
await createJestRunner({ watch: true });
|
|
73
|
+
```
|
|
74
|
+
|
|
55
75
|
---
|
|
56
76
|
|
|
57
77
|
## API
|
|
@@ -60,42 +80,81 @@ process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
|
60
80
|
|
|
61
81
|
| Option | Type | Default | Description |
|
|
62
82
|
|---|---|---|---|
|
|
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 |
|
|
83
|
+
| `projectRoot` | `string` | `process.cwd()` | Consumer project root directory |
|
|
84
|
+
| `testPathPattern` | `string` | — | Filter tests by file path pattern |
|
|
85
|
+
| `watch` | `boolean` | `false` | Enable Jest watch mode |
|
|
86
|
+
|
|
87
|
+
Returns: `Promise<AggregatedResult>` — Jest's aggregated results object.
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
---
|
|
74
90
|
|
|
75
|
-
|
|
91
|
+
## Default Configuration
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
The framework provides a complete Jest config via `getDefaultConfig(projectRoot)`:
|
|
94
|
+
|
|
95
|
+
| Setting | Value |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `testMatch` | `['**/__tests__/**/*.test.js']` |
|
|
98
|
+
| `testEnvironment` | `jsdom` |
|
|
99
|
+
| `transform` | Babel with `@babel/preset-env` |
|
|
100
|
+
| `testTimeout` | 20 000 ms |
|
|
101
|
+
| `automock` | `false` |
|
|
102
|
+
| `clearMocks` | `true` |
|
|
103
|
+
| `passWithNoTests` | `true` |
|
|
104
|
+
| `testPathIgnorePatterns` | `['/build/']` |
|
|
105
|
+
|
|
106
|
+
### Reporters
|
|
107
|
+
|
|
108
|
+
Two reporters are enabled by default:
|
|
109
|
+
|
|
110
|
+
1. **Console reporter** (`DefaultReporter`) — prints a formatted pass/fail/skip summary with elapsed time to the terminal.
|
|
111
|
+
2. **HTML reporter** (`HtmlReporter`) — generates a self-contained, interactive HTML report.
|
|
112
|
+
|
|
113
|
+
#### HTML Reporter Options
|
|
114
|
+
|
|
115
|
+
| Option | Default | Description |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `outputDir` | `<rootDir>/test-slices/unit-test/unit_reports` | Directory for the HTML file |
|
|
118
|
+
| `fileName` | `report.html` | Output filename |
|
|
119
|
+
| `title` | `Unit Test Report` | Page title |
|
|
120
|
+
|
|
121
|
+
The HTML report supports dark theme, status filters (All / Passed / Failed / Skipped), collapsible suites, and auto-expands suites with failures.
|
|
122
|
+
|
|
123
|
+
### Globals Injection
|
|
124
|
+
|
|
125
|
+
The framework's `setupFiles` automatically injects the `jest` object into `globalThis`, so test files can call `jest.fn()`, `jest.mock()`, etc. without explicit imports.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Example: CLI Integration
|
|
130
|
+
|
|
131
|
+
See [examples/consumer-cli.js](examples/consumer-cli.js) for a complete example:
|
|
86
132
|
|
|
87
|
-
Sub-path exports:
|
|
88
133
|
```js
|
|
89
|
-
import {
|
|
90
|
-
|
|
134
|
+
import { createJestRunner } from '@zohodesk/unit-testing-framework';
|
|
135
|
+
|
|
136
|
+
const command = process.argv[2];
|
|
137
|
+
|
|
138
|
+
if (command === 'unit-test') {
|
|
139
|
+
const testFile = process.argv[3];
|
|
140
|
+
const results = await createJestRunner({
|
|
141
|
+
testPathPattern: testFile,
|
|
142
|
+
});
|
|
143
|
+
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
144
|
+
} else if (command === 'unit-test:watch') {
|
|
145
|
+
await createJestRunner({ watch: true });
|
|
146
|
+
}
|
|
91
147
|
```
|
|
92
148
|
|
|
93
149
|
---
|
|
94
150
|
|
|
95
|
-
##
|
|
151
|
+
## Build
|
|
96
152
|
|
|
97
|
-
|
|
98
|
-
|
|
153
|
+
Source is written in ESM and transpiled to CommonJS via Babel (targets Node 18). Template files under `src/reporters/templates/` are excluded from transpilation and inlined at runtime.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm run build # Transpile src/ → build/
|
|
157
|
+
npm run clean # Delete build/ and rebuild
|
|
99
158
|
```
|
|
100
159
|
|
|
101
160
|
### 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
|
|
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.27-experimental",
|
|
4
4
|
"description": "A modular Jest-based unit testing framework",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"@jest/core": "30.2.0",
|
|
26
26
|
"@jest/types": "30.2.0",
|
|
27
27
|
"babel-jest": "30.2.0",
|
|
28
|
-
"jest-environment-jsdom": "30.2.0"
|
|
28
|
+
"jest-environment-jsdom": "30.2.0",
|
|
29
|
+
"jest-html-reporter": "^4.3.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@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
|
-
}
|