@zohodesk/unit-testing-framework 0.0.24-experimental → 0.0.25-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/src/reporters/default-reporter.js +6 -11
- package/build/src/reporters/html-reporter.js +14 -308
- package/build/src/reporters/html-template.js +177 -0
- package/build/src/reporters/reporter-utils.js +51 -0
- package/build/src/reporters/templates/report-script.js +44 -0
- package/build/src/reporters/templates/report.css +144 -0
- package/package.json +4 -7
- package/build/src/reporters/reporter-handler.js +0 -58
|
@@ -5,26 +5,21 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _logger = require("../utils/logger");
|
|
8
|
+
var _reporterUtils = require("./reporter-utils.js");
|
|
8
9
|
/**
|
|
9
10
|
* default-reporter.js
|
|
10
11
|
*
|
|
11
|
-
* A lightweight custom Jest reporter
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Jest Reporter interface (class-based):
|
|
15
|
-
* - onRunStart(results, options)
|
|
16
|
-
* - onTestStart(test)
|
|
17
|
-
* - onTestResult(test, testResult, results)
|
|
18
|
-
* - onRunComplete(contexts, results)
|
|
12
|
+
* A lightweight custom Jest reporter bundled with the framework.
|
|
13
|
+
* Outputs a formatted summary to the console after each test run.
|
|
19
14
|
*/
|
|
20
15
|
|
|
21
16
|
class DefaultReporter {
|
|
22
17
|
constructor(globalConfig, _reporterOptions) {
|
|
23
18
|
this.globalConfig = globalConfig;
|
|
24
|
-
this.
|
|
19
|
+
this._timer = (0, _reporterUtils.createTimer)();
|
|
25
20
|
}
|
|
26
21
|
onRunStart(_results, _options) {
|
|
27
|
-
this.
|
|
22
|
+
this._timer.start();
|
|
28
23
|
_logger.Logger.log(_logger.Logger.INFO_TYPE, '\n╔══════════════════════════════════════════╗');
|
|
29
24
|
_logger.Logger.log(_logger.Logger.INFO_TYPE, '║ Unit Testing Framework – Test Run ║');
|
|
30
25
|
_logger.Logger.log(_logger.Logger.INFO_TYPE, '╚══════════════════════════════════════════╝\n');
|
|
@@ -42,7 +37,7 @@ class DefaultReporter {
|
|
|
42
37
|
_logger.Logger.log(_logger.Logger.INFO_TYPE, ` ${icon} Passed: ${numPassingTests} Failed: ${numFailingTests} Skipped: ${numPendingTests}`);
|
|
43
38
|
}
|
|
44
39
|
onRunComplete(_contexts, results) {
|
|
45
|
-
const elapsed =
|
|
40
|
+
const elapsed = this._timer.elapsed();
|
|
46
41
|
const {
|
|
47
42
|
numPassedTests,
|
|
48
43
|
numFailedTests,
|
|
@@ -6,20 +6,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _fs = _interopRequireDefault(require("fs"));
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
|
+
var _reporterUtils = require("./reporter-utils.js");
|
|
10
|
+
var _htmlTemplate = require("./html-template.js");
|
|
9
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
12
|
/**
|
|
11
13
|
* html-reporter.js
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - Per-suite breakdown with expandable test cases
|
|
17
|
-
* - Failure messages and stack traces
|
|
18
|
-
* - Filtering controls (All / Passed / Failed / Skipped)
|
|
19
|
-
*
|
|
20
|
-
* Usage in reporters config:
|
|
21
|
-
* ['html-report'] → defaults to <projectRoot>/unit_reports/report.html
|
|
22
|
-
* ['html-report', { outputPath: '...' }] → custom output path
|
|
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.
|
|
23
18
|
*/
|
|
24
19
|
|
|
25
20
|
class HtmlReporter {
|
|
@@ -34,322 +29,33 @@ class HtmlReporter {
|
|
|
34
29
|
this.globalConfig = globalConfig;
|
|
35
30
|
this.options = reporterOptions;
|
|
36
31
|
this._suites = [];
|
|
37
|
-
this.
|
|
32
|
+
this._timer = (0, _reporterUtils.createTimer)();
|
|
38
33
|
}
|
|
39
34
|
onRunStart() {
|
|
40
|
-
this.
|
|
35
|
+
this._timer.start();
|
|
41
36
|
this._suites = [];
|
|
42
37
|
}
|
|
43
38
|
onTestResult(_test, testResult) {
|
|
44
39
|
this._suites.push(testResult);
|
|
45
40
|
}
|
|
46
41
|
onRunComplete(_contexts, aggregatedResults) {
|
|
47
|
-
const elapsed = ((Date.now() - this._startTime) / 1000).toFixed(2);
|
|
48
42
|
const rootDir = this.globalConfig.rootDir || process.cwd();
|
|
49
43
|
const outputDir = this.options.outputDir ? _path.default.resolve(rootDir, this.options.outputDir) : _path.default.resolve(rootDir, 'test-slices', 'unit-test', 'unit_reports');
|
|
50
44
|
const fileName = this.options.fileName || 'report.html';
|
|
51
45
|
const title = this.options.title || 'Unit Test Report';
|
|
52
46
|
const outputPath = _path.default.join(outputDir, fileName);
|
|
53
|
-
|
|
54
|
-
// Ensure output directory exists
|
|
55
47
|
_fs.default.mkdirSync(outputDir, {
|
|
56
48
|
recursive: true
|
|
57
49
|
});
|
|
58
|
-
const html =
|
|
50
|
+
const html = (0, _htmlTemplate.generateHtml)({
|
|
51
|
+
aggregatedResults,
|
|
52
|
+
suites: this._suites,
|
|
53
|
+
rootDir,
|
|
54
|
+
elapsed: this._timer.elapsed(),
|
|
55
|
+
title
|
|
56
|
+
});
|
|
59
57
|
_fs.default.writeFileSync(outputPath, html, 'utf-8');
|
|
60
58
|
console.log(`\n 📄 HTML report written to: ${outputPath}\n`);
|
|
61
59
|
}
|
|
62
|
-
|
|
63
|
-
// ── Private ────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
_generateHtml(results, elapsed, title) {
|
|
66
|
-
const {
|
|
67
|
-
numPassedTests,
|
|
68
|
-
numFailedTests,
|
|
69
|
-
numPendingTests,
|
|
70
|
-
numTotalTests,
|
|
71
|
-
numPassedTestSuites,
|
|
72
|
-
numFailedTestSuites,
|
|
73
|
-
numTotalTestSuites
|
|
74
|
-
} = results;
|
|
75
|
-
const suitesHtml = this._suites.map(suite => this._renderSuite(suite)).join('\n');
|
|
76
|
-
const timestamp = new Date().toLocaleString();
|
|
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>${title}</title>
|
|
83
|
-
<style>
|
|
84
|
-
:root {
|
|
85
|
-
--pass: #2ecc71;
|
|
86
|
-
--fail: #e74c3c;
|
|
87
|
-
--skip: #f39c12;
|
|
88
|
-
--bg: #1a1a2e;
|
|
89
|
-
--card: #16213e;
|
|
90
|
-
--text: #eaeaea;
|
|
91
|
-
--muted: #8892a4;
|
|
92
|
-
--border: #2a2a4a;
|
|
93
|
-
}
|
|
94
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
95
|
-
body {
|
|
96
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
|
|
97
|
-
background: var(--bg);
|
|
98
|
-
color: var(--text);
|
|
99
|
-
padding: 24px;
|
|
100
|
-
line-height: 1.5;
|
|
101
|
-
}
|
|
102
|
-
.header {
|
|
103
|
-
text-align: center;
|
|
104
|
-
margin-bottom: 32px;
|
|
105
|
-
}
|
|
106
|
-
.header h1 { font-size: 1.8rem; margin-bottom: 4px; }
|
|
107
|
-
.header .meta { color: var(--muted); font-size: 0.85rem; }
|
|
108
|
-
|
|
109
|
-
/* ── Summary Cards ─── */
|
|
110
|
-
.summary {
|
|
111
|
-
display: flex;
|
|
112
|
-
gap: 16px;
|
|
113
|
-
justify-content: center;
|
|
114
|
-
flex-wrap: wrap;
|
|
115
|
-
margin-bottom: 28px;
|
|
116
|
-
}
|
|
117
|
-
.card {
|
|
118
|
-
background: var(--card);
|
|
119
|
-
border: 1px solid var(--border);
|
|
120
|
-
border-radius: 10px;
|
|
121
|
-
padding: 18px 28px;
|
|
122
|
-
min-width: 140px;
|
|
123
|
-
text-align: center;
|
|
124
|
-
}
|
|
125
|
-
.card .value { font-size: 2rem; font-weight: 700; }
|
|
126
|
-
.card .label { font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
|
|
127
|
-
.card.pass .value { color: var(--pass); }
|
|
128
|
-
.card.fail .value { color: var(--fail); }
|
|
129
|
-
.card.skip .value { color: var(--skip); }
|
|
130
|
-
.card.total .value { color: #58a6ff; }
|
|
131
|
-
|
|
132
|
-
/* ── Filters ─── */
|
|
133
|
-
.filters {
|
|
134
|
-
display: flex;
|
|
135
|
-
gap: 8px;
|
|
136
|
-
justify-content: center;
|
|
137
|
-
margin-bottom: 24px;
|
|
138
|
-
flex-wrap: wrap;
|
|
139
|
-
}
|
|
140
|
-
.filters button {
|
|
141
|
-
background: var(--card);
|
|
142
|
-
color: var(--text);
|
|
143
|
-
border: 1px solid var(--border);
|
|
144
|
-
padding: 6px 18px;
|
|
145
|
-
border-radius: 20px;
|
|
146
|
-
cursor: pointer;
|
|
147
|
-
font-size: 0.85rem;
|
|
148
|
-
transition: all 0.2s;
|
|
149
|
-
}
|
|
150
|
-
.filters button:hover,
|
|
151
|
-
.filters button.active { background: #58a6ff; color: #000; border-color: #58a6ff; }
|
|
152
|
-
|
|
153
|
-
/* ── Suites ─── */
|
|
154
|
-
.suite {
|
|
155
|
-
background: var(--card);
|
|
156
|
-
border: 1px solid var(--border);
|
|
157
|
-
border-radius: 10px;
|
|
158
|
-
margin-bottom: 16px;
|
|
159
|
-
overflow: hidden;
|
|
160
|
-
}
|
|
161
|
-
.suite-header {
|
|
162
|
-
display: flex;
|
|
163
|
-
align-items: center;
|
|
164
|
-
padding: 14px 18px;
|
|
165
|
-
cursor: pointer;
|
|
166
|
-
user-select: none;
|
|
167
|
-
gap: 10px;
|
|
168
|
-
}
|
|
169
|
-
.suite-header:hover { background: rgba(255,255,255,0.03); }
|
|
170
|
-
.suite-header .icon { font-size: 1.1rem; }
|
|
171
|
-
.suite-header .name {
|
|
172
|
-
flex: 1;
|
|
173
|
-
font-weight: 600;
|
|
174
|
-
font-size: 0.95rem;
|
|
175
|
-
word-break: break-all;
|
|
176
|
-
}
|
|
177
|
-
.suite-header .counts { font-size: 0.8rem; color: var(--muted); }
|
|
178
|
-
.suite-header .arrow {
|
|
179
|
-
transition: transform 0.2s;
|
|
180
|
-
color: var(--muted);
|
|
181
|
-
}
|
|
182
|
-
.suite.open .suite-header .arrow { transform: rotate(90deg); }
|
|
183
|
-
|
|
184
|
-
.suite-body { display: none; border-top: 1px solid var(--border); }
|
|
185
|
-
.suite.open .suite-body { display: block; }
|
|
186
|
-
|
|
187
|
-
.test-row {
|
|
188
|
-
display: flex;
|
|
189
|
-
align-items: flex-start;
|
|
190
|
-
padding: 10px 18px 10px 36px;
|
|
191
|
-
gap: 10px;
|
|
192
|
-
border-bottom: 1px solid var(--border);
|
|
193
|
-
font-size: 0.9rem;
|
|
194
|
-
}
|
|
195
|
-
.test-row:last-child { border-bottom: none; }
|
|
196
|
-
.test-row .status {
|
|
197
|
-
flex-shrink: 0;
|
|
198
|
-
width: 20px;
|
|
199
|
-
text-align: center;
|
|
200
|
-
}
|
|
201
|
-
.test-row .test-title { flex: 1; }
|
|
202
|
-
.test-row .duration { color: var(--muted); font-size: 0.8rem; flex-shrink: 0; }
|
|
203
|
-
.test-row.passed .status { color: var(--pass); }
|
|
204
|
-
.test-row.failed .status { color: var(--fail); }
|
|
205
|
-
.test-row.pending .status { color: var(--skip); }
|
|
206
|
-
|
|
207
|
-
.failure-msg {
|
|
208
|
-
background: rgba(231, 76, 60, 0.1);
|
|
209
|
-
border-left: 3px solid var(--fail);
|
|
210
|
-
margin: 6px 0 4px 30px;
|
|
211
|
-
padding: 10px 14px;
|
|
212
|
-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
213
|
-
font-size: 0.8rem;
|
|
214
|
-
white-space: pre-wrap;
|
|
215
|
-
word-break: break-word;
|
|
216
|
-
color: #f5a5a5;
|
|
217
|
-
border-radius: 4px;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.footer {
|
|
221
|
-
text-align: center;
|
|
222
|
-
color: var(--muted);
|
|
223
|
-
font-size: 0.75rem;
|
|
224
|
-
margin-top: 32px;
|
|
225
|
-
padding-top: 16px;
|
|
226
|
-
border-top: 1px solid var(--border);
|
|
227
|
-
}
|
|
228
|
-
</style>
|
|
229
|
-
</head>
|
|
230
|
-
<body>
|
|
231
|
-
<div class="header">
|
|
232
|
-
<h1>${title}</h1>
|
|
233
|
-
<div class="meta">${timestamp} · Duration: ${elapsed}s</div>
|
|
234
|
-
</div>
|
|
235
|
-
|
|
236
|
-
<div class="summary">
|
|
237
|
-
<div class="card total"><div class="value">${numTotalTests}</div><div class="label">Total</div></div>
|
|
238
|
-
<div class="card pass"><div class="value">${numPassedTests}</div><div class="label">Passed</div></div>
|
|
239
|
-
<div class="card fail"><div class="value">${numFailedTests}</div><div class="label">Failed</div></div>
|
|
240
|
-
<div class="card skip"><div class="value">${numPendingTests}</div><div class="label">Skipped</div></div>
|
|
241
|
-
<div class="card"><div class="value">${numTotalTestSuites}</div><div class="label">Suites</div></div>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<div class="filters">
|
|
245
|
-
<button class="active" data-filter="all">All</button>
|
|
246
|
-
<button data-filter="passed">Passed</button>
|
|
247
|
-
<button data-filter="failed">Failed</button>
|
|
248
|
-
<button data-filter="pending">Skipped</button>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<div id="suites">
|
|
252
|
-
${suitesHtml}
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
<div class="footer">
|
|
256
|
-
Generated by @zohodesk/unit-testing-framework
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<script>
|
|
260
|
-
// Toggle suite expand/collapse
|
|
261
|
-
document.querySelectorAll('.suite-header').forEach(header => {
|
|
262
|
-
header.addEventListener('click', () => {
|
|
263
|
-
header.parentElement.classList.toggle('open');
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Filter buttons
|
|
268
|
-
document.querySelectorAll('.filters button').forEach(btn => {
|
|
269
|
-
btn.addEventListener('click', () => {
|
|
270
|
-
document.querySelectorAll('.filters button').forEach(b => b.classList.remove('active'));
|
|
271
|
-
btn.classList.add('active');
|
|
272
|
-
const filter = btn.dataset.filter;
|
|
273
|
-
document.querySelectorAll('.suite').forEach(suite => {
|
|
274
|
-
if (filter === 'all') {
|
|
275
|
-
suite.style.display = '';
|
|
276
|
-
suite.querySelectorAll('.test-row').forEach(r => r.style.display = '');
|
|
277
|
-
} else {
|
|
278
|
-
const rows = suite.querySelectorAll('.test-row');
|
|
279
|
-
let visible = 0;
|
|
280
|
-
rows.forEach(r => {
|
|
281
|
-
if (r.classList.contains(filter)) {
|
|
282
|
-
r.style.display = '';
|
|
283
|
-
visible++;
|
|
284
|
-
} else {
|
|
285
|
-
r.style.display = 'none';
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
suite.style.display = visible > 0 ? '' : 'none';
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Auto-expand suites with failures
|
|
295
|
-
document.querySelectorAll('.suite').forEach(suite => {
|
|
296
|
-
if (suite.querySelector('.test-row.failed')) {
|
|
297
|
-
suite.classList.add('open');
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
</script>
|
|
301
|
-
</body>
|
|
302
|
-
</html>`;
|
|
303
|
-
}
|
|
304
|
-
_renderSuite(suite) {
|
|
305
|
-
const suitePath = suite.testFilePath || 'Unknown suite';
|
|
306
|
-
// Show path relative to rootDir for readability
|
|
307
|
-
const rootDir = this.globalConfig.rootDir || process.cwd();
|
|
308
|
-
const relativePath = _path.default.relative(rootDir, suitePath);
|
|
309
|
-
const passed = suite.numPassingTests || 0;
|
|
310
|
-
const failed = suite.numFailingTests || 0;
|
|
311
|
-
const pending = suite.numPendingTests || 0;
|
|
312
|
-
const suiteIcon = failed > 0 ? '✖' : pending > 0 && passed === 0 ? '○' : '✔';
|
|
313
|
-
const suiteIconColor = failed > 0 ? 'var(--fail)' : 'var(--pass)';
|
|
314
|
-
const testsHtml = (suite.testResults || []).map(t => this._renderTest(t)).join('\n');
|
|
315
|
-
return `
|
|
316
|
-
<div class="suite">
|
|
317
|
-
<div class="suite-header">
|
|
318
|
-
<span class="icon" style="color:${suiteIconColor}">${suiteIcon}</span>
|
|
319
|
-
<span class="name">${this._escape(relativePath)}</span>
|
|
320
|
-
<span class="counts">${passed} passed · ${failed} failed · ${pending} skipped</span>
|
|
321
|
-
<span class="arrow">▶</span>
|
|
322
|
-
</div>
|
|
323
|
-
<div class="suite-body">
|
|
324
|
-
${testsHtml}
|
|
325
|
-
</div>
|
|
326
|
-
</div>`;
|
|
327
|
-
}
|
|
328
|
-
_renderTest(testResult) {
|
|
329
|
-
const status = testResult.status; // 'passed' | 'failed' | 'pending'
|
|
330
|
-
const icon = status === 'passed' ? '✔' : status === 'failed' ? '✖' : '○';
|
|
331
|
-
const duration = testResult.duration != null ? `${testResult.duration}ms` : '';
|
|
332
|
-
const title = testResult.ancestorTitles ? [...testResult.ancestorTitles, testResult.title].join(' › ') : testResult.title;
|
|
333
|
-
let failureHtml = '';
|
|
334
|
-
if (status === 'failed' && testResult.failureMessages?.length) {
|
|
335
|
-
failureHtml = testResult.failureMessages.map(msg => `<div class="failure-msg">${this._escape(this._stripAnsi(msg))}</div>`).join('\n');
|
|
336
|
-
}
|
|
337
|
-
return `
|
|
338
|
-
<div class="test-row ${status}">
|
|
339
|
-
<span class="status">${icon}</span>
|
|
340
|
-
<div class="test-title">
|
|
341
|
-
${this._escape(title)}
|
|
342
|
-
${failureHtml}
|
|
343
|
-
</div>
|
|
344
|
-
<span class="duration">${duration}</span>
|
|
345
|
-
</div>`;
|
|
346
|
-
}
|
|
347
|
-
_escape(str) {
|
|
348
|
-
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
349
|
-
}
|
|
350
|
-
_stripAnsi(str) {
|
|
351
|
-
// eslint-disable-next-line no-control-regex
|
|
352
|
-
return String(str).replace(/\x1B\[[0-9;]*m/g, '');
|
|
353
|
-
}
|
|
354
60
|
}
|
|
355
61
|
exports.default = HtmlReporter;
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
}
|
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.25-experimental",
|
|
4
4
|
"description": "A modular Jest-based unit testing framework",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -12,23 +12,20 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
14
14
|
"lint": "eslint src/ index.js",
|
|
15
|
-
"build": "babel src -d build/src && babel index.js --out-file build/index.js",
|
|
15
|
+
"build": "babel src -d build/src --copy-files && babel index.js --out-file build/index.js",
|
|
16
16
|
"prepublishOnly": "npm run build",
|
|
17
17
|
"clean": "rm -rf build && npm run build"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [],
|
|
20
20
|
"author": "",
|
|
21
|
-
"license": "
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@babel/core": "7.29.0",
|
|
24
24
|
"@babel/preset-env": "7.29.0",
|
|
25
25
|
"@jest/core": "30.2.0",
|
|
26
26
|
"@jest/types": "30.2.0",
|
|
27
27
|
"babel-jest": "30.2.0",
|
|
28
|
-
"jest-environment-
|
|
29
|
-
},
|
|
30
|
-
"peerDependencies": {
|
|
31
|
-
"jest": "30.2.0"
|
|
28
|
+
"jest-environment-jsdom": "30.2.0"
|
|
32
29
|
},
|
|
33
30
|
"devDependencies": {
|
|
34
31
|
"@babel/cli": "7.28.6",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.resolveReporters = resolveReporters;
|
|
7
|
-
var _path = _interopRequireDefault(require("path"));
|
|
8
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
/**
|
|
10
|
-
* reporter-handler.js
|
|
11
|
-
*
|
|
12
|
-
* Resolves the final reporters array for Jest configuration.
|
|
13
|
-
* All reporters are framework-controlled — consumers cannot supply custom reporters.
|
|
14
|
-
*
|
|
15
|
-
* Built-in aliases:
|
|
16
|
-
* - 'default' → Jest built-in default reporter
|
|
17
|
-
* - 'framework-default' → This package's DefaultReporter
|
|
18
|
-
* - 'html-report' → This package's HTML reporter
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// NOTE: __dirname is available after Babel transpiles ESM → CJS.
|
|
22
|
-
|
|
23
|
-
const BUILTIN_ALIASES = {
|
|
24
|
-
'framework-default': _path.default.resolve(__dirname, 'default-reporter.js'),
|
|
25
|
-
'html-report': _path.default.resolve(__dirname, 'html-reporter.js')
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Resolve a single reporter entry.
|
|
30
|
-
*
|
|
31
|
-
* @param {string | [string, object]} entry
|
|
32
|
-
* @returns {string | [string, object]}
|
|
33
|
-
*/
|
|
34
|
-
function resolveEntry(entry) {
|
|
35
|
-
const isArray = Array.isArray(entry);
|
|
36
|
-
const name = isArray ? entry[0] : entry;
|
|
37
|
-
const opts = isArray ? entry[1] : undefined;
|
|
38
|
-
|
|
39
|
-
// Check built-in aliases
|
|
40
|
-
if (BUILTIN_ALIASES[name]) {
|
|
41
|
-
const resolved = BUILTIN_ALIASES[name];
|
|
42
|
-
return opts ? [resolved, opts] : resolved;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 'default' and any other entries pass through as-is
|
|
46
|
-
return entry;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the reporters array for final Jest config.
|
|
51
|
-
*
|
|
52
|
-
* @param {Array<string | [string, object]>} reporters - Raw reporters from config.
|
|
53
|
-
* @param {string} [projectRoot] - Consumer project root (unused, kept for API compatibility).
|
|
54
|
-
* @returns {Array<string | [string, object]>}
|
|
55
|
-
*/
|
|
56
|
-
function resolveReporters(reporters = ['default'], projectRoot) {
|
|
57
|
-
return reporters.map(entry => resolveEntry(entry));
|
|
58
|
-
}
|