@zohodesk/unit-testing-framework 0.0.24-experimental → 0.0.26-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.
@@ -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 that can be bundled with the framework.
12
- * Consumer projects can use this as-is or supply their own reporters.
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._startTime = 0;
19
+ this._timer = (0, _reporterUtils.createTimer)();
25
20
  }
26
21
  onRunStart(_results, _options) {
27
- this._startTime = Date.now();
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 = ((Date.now() - this._startTime) / 1000).toFixed(2);
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
- * A zero-dependency custom Jest reporter that generates a self-contained
14
- * HTML report after each test run. The report includes:
15
- * - Summary (total / passed / failed / skipped / duration)
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._startTime = 0;
32
+ this._timer = (0, _reporterUtils.createTimer)();
38
33
  }
39
34
  onRunStart() {
40
- this._startTime = Date.now();
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 = this._generateHtml(aggregatedResults, elapsed, title);
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} &nbsp;·&nbsp; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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} &nbsp;·&nbsp; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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.24-experimental",
3
+ "version": "0.0.26-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": "MIT",
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-node": "30.2.0"
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
- }