@zohodesk/unit-testing-framework 0.0.7-experimental → 0.0.9-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/html-reporter.js +26 -233
- package/package.json +1 -1
|
@@ -17,16 +17,21 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
17
17
|
* - Failure messages and stack traces
|
|
18
18
|
* - Filtering controls (All / Passed / Failed / Skipped)
|
|
19
19
|
*
|
|
20
|
+
* The HTML layout lives in report-template.html (same directory).
|
|
21
|
+
* This reporter reads the template and replaces {{PLACEHOLDER}} tokens.
|
|
22
|
+
*
|
|
20
23
|
* Usage in reporters config:
|
|
21
|
-
* ['html-report']
|
|
22
|
-
* ['html-report', {
|
|
24
|
+
* ['html-report'] → defaults
|
|
25
|
+
* ['html-report', { outputDir: 'custom/path', fileName: 'results.html' }] → custom
|
|
23
26
|
*/
|
|
24
27
|
|
|
28
|
+
// NOTE: __dirname is available after Babel transpiles ESM → CJS.
|
|
29
|
+
const TEMPLATE_PATH = _path.default.resolve(__dirname, 'report-template.html');
|
|
25
30
|
class HtmlReporter {
|
|
26
31
|
/**
|
|
27
32
|
* @param {object} globalConfig - Jest global config
|
|
28
33
|
* @param {object} reporterOptions
|
|
29
|
-
* @param {string} [reporterOptions.outputDir] - Directory for the HTML file (default: <
|
|
34
|
+
* @param {string} [reporterOptions.outputDir] - Directory for the HTML file (default: <cwd>/test-slices/unit-test/unit_reports)
|
|
30
35
|
* @param {string} [reporterOptions.fileName] - HTML file name (default: report.html)
|
|
31
36
|
* @param {string} [reporterOptions.title] - Page title (default: Unit Test Report)
|
|
32
37
|
*/
|
|
@@ -45,8 +50,7 @@ class HtmlReporter {
|
|
|
45
50
|
}
|
|
46
51
|
onRunComplete(_contexts, aggregatedResults) {
|
|
47
52
|
const elapsed = ((Date.now() - this._startTime) / 1000).toFixed(2);
|
|
48
|
-
const
|
|
49
|
-
const outputDir = this.options.outputDir ? _path.default.resolve(rootDir, this.options.outputDir) : _path.default.resolve(rootDir, 'unit_reports');
|
|
53
|
+
const outputDir = _path.default.resolve(process.cwd(), this.options.outputDir || 'test-slices/unit-test/unit_reports');
|
|
50
54
|
const fileName = this.options.fileName || 'report.html';
|
|
51
55
|
const title = this.options.title || 'Unit Test Report';
|
|
52
56
|
const outputPath = _path.default.join(outputDir, fileName);
|
|
@@ -68,242 +72,31 @@ class HtmlReporter {
|
|
|
68
72
|
numFailedTests,
|
|
69
73
|
numPendingTests,
|
|
70
74
|
numTotalTests,
|
|
71
|
-
numPassedTestSuites,
|
|
72
|
-
numFailedTestSuites,
|
|
73
75
|
numTotalTestSuites
|
|
74
76
|
} = results;
|
|
75
77
|
const suitesHtml = this._suites.map(suite => this._renderSuite(suite)).join('\n');
|
|
76
78
|
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
79
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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>`;
|
|
80
|
+
// Read the HTML template and replace placeholders
|
|
81
|
+
let template = _fs.default.readFileSync(TEMPLATE_PATH, 'utf-8');
|
|
82
|
+
const replacements = {
|
|
83
|
+
'{{TITLE}}': title,
|
|
84
|
+
'{{TIMESTAMP}}': timestamp,
|
|
85
|
+
'{{ELAPSED}}': elapsed,
|
|
86
|
+
'{{NUM_TOTAL_TESTS}}': numTotalTests,
|
|
87
|
+
'{{NUM_PASSED_TESTS}}': numPassedTests,
|
|
88
|
+
'{{NUM_FAILED_TESTS}}': numFailedTests,
|
|
89
|
+
'{{NUM_PENDING_TESTS}}': numPendingTests,
|
|
90
|
+
'{{NUM_TOTAL_SUITES}}': numTotalTestSuites,
|
|
91
|
+
'{{SUITES_HTML}}': suitesHtml
|
|
92
|
+
};
|
|
93
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
94
|
+
template = template.replaceAll(placeholder, String(value));
|
|
95
|
+
}
|
|
96
|
+
return template;
|
|
303
97
|
}
|
|
304
98
|
_renderSuite(suite) {
|
|
305
99
|
const suitePath = suite.testFilePath || 'Unknown suite';
|
|
306
|
-
// Show path relative to rootDir for readability
|
|
307
100
|
const rootDir = this.globalConfig.rootDir || process.cwd();
|
|
308
101
|
const relativePath = _path.default.relative(rootDir, suitePath);
|
|
309
102
|
const passed = suite.numPassingTests || 0;
|