@zohodesk/unit-testing-framework 0.0.25-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 CHANGED
@@ -1,6 +1,6 @@
1
- # unit-testing-framework
1
+ # @zohodesk/unit-testing-framework
2
2
 
3
- A modular, Jest-based unit testing package designed to plug into existing CLI pipelines. Runs Jest **programmatically** (no shell execution), supports default + consumer config merging, custom reporters, global setup/teardown, and parallel execution.
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 # ESM package with proper exports
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
- │ │ └── jest-runner.js # Programmatic Jest execution via @jest/core
16
+ │ │ ├── jest-runner.js # createJestRunner() main orchestrator
17
+ │ │ └── runner-base.js # buildArgv() & executeJest() via @jest/core
16
18
  │ ├── config/
17
- │ │ ├── config-loader.js # Config resolution & deep-merge logic
18
- │ │ └── default-config.js # Framework-level default Jest config
19
+ │ │ └── default-config.js # getDefaultConfig() framework defaults
19
20
  │ ├── reporters/
20
- │ │ ├── reporter-handler.js # Reporter resolution (aliases, paths)
21
- │ │ └── default-reporter.js # Bundled custom Jest reporter
22
- └── environment/
23
- ├── setup.js # Global setup (runs once before suite)
24
- └── teardown.js # Global teardown (runs once after suite)
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
- ├── consumer-cli.js # Example integration in existing CLI
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 with defaults - auto-discovers jest.unit.config.js in project root
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
- | `configPath` | `string` | (auto-discovered) | Path to consumer config file |
65
- | `inlineConfig` | `object` | `{}` | Inline Jest config overrides (highest priority) |
66
- | `coverage` | `boolean` | from config | Enable coverage collection |
67
- | `testFiles` | `string[]` | all | Specific test file patterns |
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
- Returns: `Promise<AggregatedResult>` (Jest results object)
89
+ ---
74
90
 
75
- ### Named exports
91
+ ## Default Configuration
76
92
 
77
- ```js
78
- import {
79
- createJestRunner,
80
- loadConfig,
81
- resolveReporters,
82
- globalSetup,
83
- globalTeardown,
84
- } from 'unit-testing-framework';
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 { loadConfig } from 'unit-testing-framework/config';
90
- import { resolveReporters } from 'unit-testing-framework/reporters';
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
- ## Config Priority
151
+ ## Build
96
152
 
97
- ```
98
- inline options > consumer config file > framework defaults
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', _path.default.resolve(__dirname, '..', 'reporters', 'html-reporter.js')],
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.25-experimental",
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} &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
- }
@@ -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, '&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
- }
@@ -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
- }