@zohodesk/unit-testing-framework 1.0.0
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 +212 -0
- package/index.js +20 -0
- package/package.json +46 -0
- package/src/config/config-loader.js +137 -0
- package/src/config/default-config.js +77 -0
- package/src/environment/globals-inject.js +15 -0
- package/src/environment/setup.js +16 -0
- package/src/environment/teardown.js +19 -0
- package/src/reporters/default-reporter.js +50 -0
- package/src/reporters/reporter-handler.js +60 -0
- package/src/runner/jest-runner.js +114 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# unit-testing-framework
|
|
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.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Folder Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
unit-testing-framework/
|
|
11
|
+
├── package.json # ESM package with proper exports
|
|
12
|
+
├── index.js # Public API entry point
|
|
13
|
+
├── src/
|
|
14
|
+
│ ├── runner/
|
|
15
|
+
│ │ └── jest-runner.js # Programmatic Jest execution via @jest/core
|
|
16
|
+
│ ├── config/
|
|
17
|
+
│ │ ├── config-loader.js # Config resolution & deep-merge logic
|
|
18
|
+
│ │ └── default-config.js # Framework-level default Jest config
|
|
19
|
+
│ ├── 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)
|
|
25
|
+
├── examples/
|
|
26
|
+
│ ├── consumer-cli.js # Example integration in existing CLI
|
|
27
|
+
│ └── jest.unit.config.js # Example consumer config file
|
|
28
|
+
├── .npmignore
|
|
29
|
+
└── README.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install unit-testing-framework
|
|
38
|
+
# jest is a required peer dependency
|
|
39
|
+
npm install --save-dev jest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import createJestRunner from 'unit-testing-framework';
|
|
48
|
+
|
|
49
|
+
// Run with defaults - auto-discovers jest.unit.config.js in project root
|
|
50
|
+
const results = await createJestRunner();
|
|
51
|
+
|
|
52
|
+
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `createJestRunner(options?)`
|
|
60
|
+
|
|
61
|
+
| Option | Type | Default | Description |
|
|
62
|
+
|---|---|---|---|
|
|
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 |
|
|
72
|
+
|
|
73
|
+
Returns: `Promise<AggregatedResult>` (Jest results object)
|
|
74
|
+
|
|
75
|
+
### Named exports
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import {
|
|
79
|
+
createJestRunner,
|
|
80
|
+
loadConfig,
|
|
81
|
+
resolveReporters,
|
|
82
|
+
globalSetup,
|
|
83
|
+
globalTeardown,
|
|
84
|
+
} from 'unit-testing-framework';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Sub-path exports:
|
|
88
|
+
```js
|
|
89
|
+
import { loadConfig } from 'unit-testing-framework/config';
|
|
90
|
+
import { resolveReporters } from 'unit-testing-framework/reporters';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Config Priority
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
inline options > consumer config file > framework defaults
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Consumer Config File
|
|
102
|
+
|
|
103
|
+
Place one of these in your project root (auto-discovered in order):
|
|
104
|
+
|
|
105
|
+
1. `jest.unit.config.js`
|
|
106
|
+
2. `jest.unit.config.mjs`
|
|
107
|
+
3. `jest.unit.config.json`
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
// jest.unit.config.js
|
|
111
|
+
export default {
|
|
112
|
+
roots: ['<rootDir>/tests/unit'],
|
|
113
|
+
maxWorkers: 4,
|
|
114
|
+
collectCoverage: true,
|
|
115
|
+
reporters: ['default', 'framework-default'],
|
|
116
|
+
testTimeout: 60_000,
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Built-in Reporter Alias
|
|
121
|
+
|
|
122
|
+
Use `'framework-default'` in the `reporters` array to include the bundled reporter.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Consumer CLI Integration
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
// In your existing CLI file
|
|
130
|
+
import createJestRunner from 'unit-testing-framework';
|
|
131
|
+
|
|
132
|
+
switch (option) {
|
|
133
|
+
case 'unit-test': {
|
|
134
|
+
const results = await createJestRunner();
|
|
135
|
+
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case 'unit-test:coverage': {
|
|
140
|
+
const results = await createJestRunner({ coverage: true });
|
|
141
|
+
process.exitCode = results.numFailedTests > 0 ? 1 : 0;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'unit-test:watch':
|
|
146
|
+
await createJestRunner({ watch: true });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Consumer Project Structure Example
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
my-app/
|
|
157
|
+
├── package.json # depends on unit-testing-framework
|
|
158
|
+
├── jest.unit.config.js # optional overrides
|
|
159
|
+
├── cli.js # existing CLI with switch/case
|
|
160
|
+
├── src/
|
|
161
|
+
│ └── utils/
|
|
162
|
+
│ └── math.js
|
|
163
|
+
└── tests/
|
|
164
|
+
└── unit/
|
|
165
|
+
└── math.test.js
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Publishing
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# 1. Login to npm
|
|
174
|
+
npm login
|
|
175
|
+
|
|
176
|
+
# 2. Verify package contents
|
|
177
|
+
npm pack --dry-run
|
|
178
|
+
|
|
179
|
+
# 3. Publish
|
|
180
|
+
npm publish
|
|
181
|
+
|
|
182
|
+
# For scoped packages:
|
|
183
|
+
npm publish --access public
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Versioning
|
|
187
|
+
|
|
188
|
+
Follow [semver](https://semver.org/):
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm version patch # 1.0.0 -> 1.0.1 (bug fixes)
|
|
192
|
+
npm version minor # 1.0.0 -> 1.1.0 (new features, backward-compatible)
|
|
193
|
+
npm version major # 1.0.0 -> 2.0.0 (breaking changes)
|
|
194
|
+
npm publish
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Consumer pins via package.json:
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"dependencies": {
|
|
201
|
+
"unit-testing-framework": "^1.0.0"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`^1.0.0` allows auto-upgrade for minor/patch. Use exact pinning (`1.0.0`) for strict control.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unit-testing-framework
|
|
3
|
+
*
|
|
4
|
+
* A modular Jest-based unit testing framework that plugs into
|
|
5
|
+
* existing CLI pipelines via a single exported function.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import createJestRunner from 'unit-testing-framework';
|
|
9
|
+
* const results = await createJestRunner({ configPath: './jest.config.js' });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { default as createJestRunner } from './src/runner/jest-runner.js';
|
|
13
|
+
export { loadConfig } from './src/config/config-loader.js';
|
|
14
|
+
export { resolveReporters } from './src/reporters/reporter-handler.js';
|
|
15
|
+
export { default as globalSetup } from './src/environment/setup.js';
|
|
16
|
+
export { default as globalTeardown } from './src/environment/teardown.js';
|
|
17
|
+
|
|
18
|
+
// Default export for simple consumer usage:
|
|
19
|
+
// import createJestRunner from 'unit-testing-framework';
|
|
20
|
+
export { default } from './src/runner/jest-runner.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zohodesk/unit-testing-framework",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A modular Jest-based unit testing framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./config": "./src/config/config-loader.js",
|
|
10
|
+
"./reporters": "./src/reporters/reporter-handler.js",
|
|
11
|
+
"./runner": "./src/runner/jest-runner.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"src/**/*.js"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
19
|
+
"lint": "eslint src/ index.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"jest",
|
|
23
|
+
"unit-testing",
|
|
24
|
+
"test-runner",
|
|
25
|
+
"testing-framework",
|
|
26
|
+
"programmatic-jest"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@jest/core": "29.7.0",
|
|
32
|
+
"@jest/types": "29.6.3",
|
|
33
|
+
"jest-environment-node": "29.7.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"jest": "29.7.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"jest": {
|
|
40
|
+
"optional": false
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"jest": "29.7.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-loader.js
|
|
3
|
+
*
|
|
4
|
+
* Responsible for:
|
|
5
|
+
* 1. Loading the framework default config.
|
|
6
|
+
* 2. Loading optional consumer-level config (file or inline).
|
|
7
|
+
* 3. Deep-merging them with consumer config taking priority.
|
|
8
|
+
*
|
|
9
|
+
* Merge priority (highest → lowest):
|
|
10
|
+
* inline options > consumer config file > framework defaults
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { pathToFileURL } from 'url';
|
|
16
|
+
import { getDefaultConfig } from './default-config.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Deep-merge two plain objects. Arrays are concatenated & de-duped.
|
|
20
|
+
* `source` values override `target` values.
|
|
21
|
+
*/
|
|
22
|
+
function deepMerge(target, source) {
|
|
23
|
+
const output = { ...target };
|
|
24
|
+
|
|
25
|
+
for (const key of Object.keys(source)) {
|
|
26
|
+
const srcVal = source[key];
|
|
27
|
+
const tgtVal = target[key];
|
|
28
|
+
|
|
29
|
+
if (srcVal === undefined) continue;
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(srcVal) && Array.isArray(tgtVal)) {
|
|
32
|
+
// Concatenate and de-duplicate
|
|
33
|
+
output[key] = [...new Set([...tgtVal, ...srcVal])];
|
|
34
|
+
} else if (
|
|
35
|
+
srcVal !== null &&
|
|
36
|
+
typeof srcVal === 'object' &&
|
|
37
|
+
!Array.isArray(srcVal) &&
|
|
38
|
+
tgtVal !== null &&
|
|
39
|
+
typeof tgtVal === 'object' &&
|
|
40
|
+
!Array.isArray(tgtVal)
|
|
41
|
+
) {
|
|
42
|
+
output[key] = deepMerge(tgtVal, srcVal);
|
|
43
|
+
} else {
|
|
44
|
+
output[key] = srcVal;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Attempt to load a consumer config file.
|
|
53
|
+
* Supports: .js (ESM), .mjs, .json, .ts (if ts-node is available).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} configPath - Absolute path to the config file.
|
|
56
|
+
* @returns {Promise<object>} Loaded configuration or empty object.
|
|
57
|
+
*/
|
|
58
|
+
async function loadConfigFile(configPath) {
|
|
59
|
+
if (!fs.existsSync(configPath)) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (ext === '.json') {
|
|
66
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ESM dynamic import works for .js / .mjs
|
|
71
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
72
|
+
const mod = await import(fileUrl);
|
|
73
|
+
return mod.default ?? mod;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the consumer config file path.
|
|
78
|
+
* Search order:
|
|
79
|
+
* 1. Explicit `configPath` option
|
|
80
|
+
* 2. `jest.unit.config.js` in project root
|
|
81
|
+
* 3. `jest.unit.config.mjs` in project root
|
|
82
|
+
* 4. `jest.unit.config.json` in project root
|
|
83
|
+
*
|
|
84
|
+
* @param {string} projectRoot
|
|
85
|
+
* @param {string} [explicitPath]
|
|
86
|
+
* @returns {string|null}
|
|
87
|
+
*/
|
|
88
|
+
function resolveConfigFilePath(projectRoot, explicitPath) {
|
|
89
|
+
if (explicitPath) {
|
|
90
|
+
const abs = path.isAbsolute(explicitPath)
|
|
91
|
+
? explicitPath
|
|
92
|
+
: path.resolve(projectRoot, explicitPath);
|
|
93
|
+
return fs.existsSync(abs) ? abs : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidates = [
|
|
97
|
+
'jest.unit.config.js',
|
|
98
|
+
'jest.unit.config.mjs',
|
|
99
|
+
'jest.unit.config.json',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const name of candidates) {
|
|
103
|
+
const full = path.resolve(projectRoot, name);
|
|
104
|
+
if (fs.existsSync(full)) return full;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load and merge configuration.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} options
|
|
114
|
+
* @param {string} options.projectRoot - Consumer project root (default: process.cwd()).
|
|
115
|
+
* @param {string} [options.configPath] - Explicit path to consumer config.
|
|
116
|
+
* @param {object} [options.inlineConfig] - Inline overrides (highest priority).
|
|
117
|
+
* @returns {Promise<import('@jest/types').Config.InitialOptions>}
|
|
118
|
+
*/
|
|
119
|
+
export async function loadConfig({
|
|
120
|
+
projectRoot = process.cwd(),
|
|
121
|
+
configPath,
|
|
122
|
+
inlineConfig = {},
|
|
123
|
+
} = {}) {
|
|
124
|
+
// 1. Framework defaults
|
|
125
|
+
const defaults = getDefaultConfig(projectRoot);
|
|
126
|
+
|
|
127
|
+
// 2. Consumer config file
|
|
128
|
+
const resolvedPath = resolveConfigFilePath(projectRoot, configPath);
|
|
129
|
+
const consumerFileConfig = resolvedPath
|
|
130
|
+
? await loadConfigFile(resolvedPath)
|
|
131
|
+
: {};
|
|
132
|
+
|
|
133
|
+
// 3. Merge: defaults ← consumer file ← inline
|
|
134
|
+
const merged = deepMerge(deepMerge(defaults, consumerFileConfig), inlineConfig);
|
|
135
|
+
|
|
136
|
+
return merged;
|
|
137
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* default-config.js
|
|
3
|
+
*
|
|
4
|
+
* Framework-level default Jest configuration.
|
|
5
|
+
* Consumer projects can override any of these values
|
|
6
|
+
* via their own config file or inline options.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the default Jest configuration object.
|
|
17
|
+
* Paths are resolved relative to the consumer's project root
|
|
18
|
+
* (passed at runtime), NOT relative to this package.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} projectRoot - Absolute path to the consumer project root.
|
|
21
|
+
* @returns {import('@jest/types').Config.InitialOptions}
|
|
22
|
+
*/
|
|
23
|
+
export function getDefaultConfig(projectRoot) {
|
|
24
|
+
return {
|
|
25
|
+
// --------------- Roots & File Discovery ---------------
|
|
26
|
+
roots: [path.resolve(projectRoot, 'tests')],
|
|
27
|
+
testMatch: [
|
|
28
|
+
'**/*.test.js',
|
|
29
|
+
'**/*.test.mjs',
|
|
30
|
+
'**/*.test.ts',
|
|
31
|
+
'**/*.spec.js',
|
|
32
|
+
'**/*.spec.mjs',
|
|
33
|
+
'**/*.spec.ts',
|
|
34
|
+
],
|
|
35
|
+
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/build/'],
|
|
36
|
+
|
|
37
|
+
// --------------- Environment ---------------
|
|
38
|
+
testEnvironment: 'node',
|
|
39
|
+
|
|
40
|
+
// --------------- Transform ---------------
|
|
41
|
+
// ESM-native – no transform needed for plain JS.
|
|
42
|
+
// Consumer can add ts-jest / babel-jest as needed.
|
|
43
|
+
transform: {},
|
|
44
|
+
|
|
45
|
+
// --------------- Coverage ---------------
|
|
46
|
+
collectCoverage: false,
|
|
47
|
+
coverageDirectory: path.resolve(projectRoot, 'coverage'),
|
|
48
|
+
coverageReporters: ['text', 'lcov', 'clover'],
|
|
49
|
+
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
|
|
50
|
+
|
|
51
|
+
// --------------- Reporters ---------------
|
|
52
|
+
reporters: ['default'],
|
|
53
|
+
|
|
54
|
+
// --------------- Parallelism & Performance ---------------
|
|
55
|
+
maxWorkers: '50%', // Use half of available CPUs
|
|
56
|
+
maxConcurrency: 5,
|
|
57
|
+
|
|
58
|
+
// --------------- Timeouts ---------------
|
|
59
|
+
testTimeout: 30_000, // 30 seconds per test
|
|
60
|
+
|
|
61
|
+
// --------------- Setup Files ---------------
|
|
62
|
+
// Injects `jest` into globalThis so ESM test files don't need
|
|
63
|
+
// `import { jest } from '@jest/globals'` manually.
|
|
64
|
+
setupFiles: [path.resolve(__dirname, '..', 'environment', 'globals-inject.js')],
|
|
65
|
+
|
|
66
|
+
// --------------- Global Setup / Teardown ---------------
|
|
67
|
+
globalSetup: path.resolve(__dirname, '..', 'environment', 'setup.js'),
|
|
68
|
+
globalTeardown: path.resolve(__dirname, '..', 'environment', 'teardown.js'),
|
|
69
|
+
|
|
70
|
+
// --------------- Module Resolution ---------------
|
|
71
|
+
moduleFileExtensions: ['js', 'mjs', 'ts', 'json', 'node'],
|
|
72
|
+
|
|
73
|
+
// --------------- Misc ---------------
|
|
74
|
+
verbose: true,
|
|
75
|
+
passWithNoTests: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* globals-inject.js
|
|
3
|
+
*
|
|
4
|
+
* Framework-level setup file that injects Jest globals (like `jest`)
|
|
5
|
+
* into `globalThis` so that ESM test files can use `jest.fn()`,
|
|
6
|
+
* `jest.mock()`, etc. without needing an explicit import.
|
|
7
|
+
*
|
|
8
|
+
* This file is referenced in the default config's `setupFiles` array.
|
|
9
|
+
* It runs in the test worker context after the environment is
|
|
10
|
+
* installed, making `@jest/globals` available.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { jest } from '@jest/globals';
|
|
14
|
+
|
|
15
|
+
globalThis.jest = jest;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Setup
|
|
3
|
+
*
|
|
4
|
+
* Runs once before the entire test suite.
|
|
5
|
+
* Jest calls this module's default export.
|
|
6
|
+
*
|
|
7
|
+
* Consumer projects can override by setting `globalSetup`
|
|
8
|
+
* in their jest.unit.config.js to their own setup file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default async function globalSetup(_globalConfig) {
|
|
12
|
+
// Store start timestamp for reporting
|
|
13
|
+
process.env.__UTL_START_TIME__ = Date.now().toString();
|
|
14
|
+
|
|
15
|
+
console.log('[unit-testing-framework] Global setup complete.');
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Teardown
|
|
3
|
+
*
|
|
4
|
+
* Runs once after the entire test suite completes.
|
|
5
|
+
* Jest calls this module's default export.
|
|
6
|
+
*
|
|
7
|
+
* Consumer projects can override by setting `globalTeardown`
|
|
8
|
+
* in their jest.unit.config.js to their own teardown file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default async function globalTeardown(_globalConfig) {
|
|
12
|
+
const startTime = Number(process.env.__UTL_START_TIME__ || Date.now());
|
|
13
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
14
|
+
|
|
15
|
+
console.log(`[unit-testing-framework] Global teardown complete. Total time: ${elapsed}s`);
|
|
16
|
+
|
|
17
|
+
// Cleanup environment variable
|
|
18
|
+
delete process.env.__UTL_START_TIME__;
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* default-reporter.js
|
|
3
|
+
*
|
|
4
|
+
* A lightweight custom Jest reporter that can be bundled with the framework.
|
|
5
|
+
* Consumer projects can use this as-is or supply their own reporters.
|
|
6
|
+
*
|
|
7
|
+
* Jest Reporter interface (class-based):
|
|
8
|
+
* - onRunStart(results, options)
|
|
9
|
+
* - onTestStart(test)
|
|
10
|
+
* - onTestResult(test, testResult, results)
|
|
11
|
+
* - onRunComplete(contexts, results)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default class DefaultReporter {
|
|
15
|
+
constructor(globalConfig, _reporterOptions) {
|
|
16
|
+
this.globalConfig = globalConfig;
|
|
17
|
+
this._startTime = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onRunStart(_results, _options) {
|
|
21
|
+
this._startTime = Date.now();
|
|
22
|
+
console.log('\n╔══════════════════════════════════════════╗');
|
|
23
|
+
console.log('║ Unit Testing Framework – Test Run ║');
|
|
24
|
+
console.log('╚══════════════════════════════════════════╝\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onTestStart(test) {
|
|
28
|
+
console.log(` ▶ Running: ${test.path}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onTestResult(_test, testResult, _aggregatedResults) {
|
|
32
|
+
const { numPassingTests, numFailingTests, numPendingTests } = testResult;
|
|
33
|
+
const icon = numFailingTests > 0 ? '✖' : '✔';
|
|
34
|
+
console.log(
|
|
35
|
+
` ${icon} Passed: ${numPassingTests} Failed: ${numFailingTests} Skipped: ${numPendingTests}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onRunComplete(_contexts, results) {
|
|
40
|
+
const elapsed = ((Date.now() - this._startTime) / 1000).toFixed(2);
|
|
41
|
+
const { numPassedTests, numFailedTests, numTotalTests } = results;
|
|
42
|
+
|
|
43
|
+
console.log('\n──────────────────────────────────────────');
|
|
44
|
+
console.log(` Total: ${numTotalTests}`);
|
|
45
|
+
console.log(` Passed: ${numPassedTests}`);
|
|
46
|
+
console.log(` Failed: ${numFailedTests}`);
|
|
47
|
+
console.log(` Time: ${elapsed}s`);
|
|
48
|
+
console.log('──────────────────────────────────────────\n');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reporter-handler.js
|
|
3
|
+
*
|
|
4
|
+
* Resolves the final reporters array for Jest configuration.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - 'default' → Jest built-in default reporter
|
|
8
|
+
* - 'framework-default' → This package's DefaultReporter
|
|
9
|
+
* - Absolute paths → Consumer-supplied reporter modules
|
|
10
|
+
* - [reporterPath, options] tuples
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const BUILTIN_ALIASES = {
|
|
20
|
+
'framework-default': path.resolve(__dirname, 'default-reporter.js'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a single reporter entry.
|
|
25
|
+
*
|
|
26
|
+
* @param {string | [string, object]} entry
|
|
27
|
+
* @param {string} projectRoot
|
|
28
|
+
* @returns {string | [string, object]}
|
|
29
|
+
*/
|
|
30
|
+
function resolveEntry(entry, projectRoot) {
|
|
31
|
+
const isArray = Array.isArray(entry);
|
|
32
|
+
const name = isArray ? entry[0] : entry;
|
|
33
|
+
const opts = isArray ? entry[1] : undefined;
|
|
34
|
+
|
|
35
|
+
// Check built-in aliases
|
|
36
|
+
if (BUILTIN_ALIASES[name]) {
|
|
37
|
+
const resolved = BUILTIN_ALIASES[name];
|
|
38
|
+
return opts ? [resolved, opts] : resolved;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 'default' and package names are passed through as-is
|
|
42
|
+
if (name === 'default' || !name.startsWith('.')) {
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Relative paths → resolve from consumer project root
|
|
47
|
+
const abs = path.resolve(projectRoot, name);
|
|
48
|
+
return opts ? [abs, opts] : abs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the reporters array for final Jest config.
|
|
53
|
+
*
|
|
54
|
+
* @param {Array<string | [string, object]>} reporters - Raw reporters from merged config.
|
|
55
|
+
* @param {string} projectRoot - Consumer project root.
|
|
56
|
+
* @returns {Array<string | [string, object]>}
|
|
57
|
+
*/
|
|
58
|
+
export function resolveReporters(reporters = ['default'], projectRoot = process.cwd()) {
|
|
59
|
+
return reporters.map((entry) => resolveEntry(entry, projectRoot));
|
|
60
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jest-runner.js
|
|
3
|
+
*
|
|
4
|
+
* Core module that runs Jest programmatically via @jest/core's runCLI.
|
|
5
|
+
*
|
|
6
|
+
* Exported as default from the package's index.js so consumers do:
|
|
7
|
+
* import createJestRunner from 'unit-testing-framework';
|
|
8
|
+
* await createJestRunner({ projectRoot: process.cwd() });
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { loadConfig } from '../config/config-loader.js';
|
|
13
|
+
import { resolveReporters } from '../reporters/reporter-handler.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} RunnerOptions
|
|
17
|
+
* @property {string} [projectRoot] - Absolute path to the consumer project (default: cwd).
|
|
18
|
+
* @property {string} [configPath] - Path to consumer jest.unit.config.{js,mjs,json}.
|
|
19
|
+
* @property {object} [inlineConfig] - Inline Jest config overrides (highest priority).
|
|
20
|
+
* @property {boolean} [coverage] - Enable coverage collection.
|
|
21
|
+
* @property {string[]} [testFiles] - Specific test file patterns to run.
|
|
22
|
+
* @property {boolean} [verbose] - Verbose output.
|
|
23
|
+
* @property {number|string} [maxWorkers] - Worker concurrency (e.g. '50%' or 4).
|
|
24
|
+
* @property {boolean} [silent] - Suppress console output from tests.
|
|
25
|
+
* @property {boolean} [watch] - Run in watch mode.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create and execute a Jest test run.
|
|
30
|
+
*
|
|
31
|
+
* @param {RunnerOptions} [options={}]
|
|
32
|
+
* @returns {Promise<import('@jest/core').AggregatedResult>} Jest aggregated results.
|
|
33
|
+
*/
|
|
34
|
+
export default async function createJestRunner(options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
projectRoot = process.cwd(),
|
|
37
|
+
configPath,
|
|
38
|
+
inlineConfig = {},
|
|
39
|
+
coverage,
|
|
40
|
+
testFiles,
|
|
41
|
+
verbose,
|
|
42
|
+
maxWorkers,
|
|
43
|
+
silent,
|
|
44
|
+
watch = false,
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
// ── 1. Load & merge configuration ──────────────────────────
|
|
48
|
+
const mergedConfig = await loadConfig({
|
|
49
|
+
projectRoot,
|
|
50
|
+
configPath,
|
|
51
|
+
inlineConfig,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── 2. Apply CLI-level overrides ───────────────────────────
|
|
55
|
+
if (coverage !== undefined) mergedConfig.collectCoverage = coverage;
|
|
56
|
+
if (verbose !== undefined) mergedConfig.verbose = verbose;
|
|
57
|
+
if (maxWorkers !== undefined) mergedConfig.maxWorkers = maxWorkers;
|
|
58
|
+
if (silent !== undefined) mergedConfig.silent = silent;
|
|
59
|
+
|
|
60
|
+
// ── 3. Resolve reporters ───────────────────────────────────
|
|
61
|
+
mergedConfig.reporters = resolveReporters(mergedConfig.reporters, projectRoot);
|
|
62
|
+
|
|
63
|
+
// ── 4. Build argv for runCLI ───────────────────────────────
|
|
64
|
+
// runCLI expects a yargs-like argv object.
|
|
65
|
+
const argv = buildArgv(mergedConfig, { testFiles, watch, projectRoot });
|
|
66
|
+
|
|
67
|
+
// ── 5. Run Jest programmatically ───────────────────────────
|
|
68
|
+
// Lazy-import to keep startup fast; @jest/core is heavy.
|
|
69
|
+
const { runCLI } = await import('@jest/core');
|
|
70
|
+
|
|
71
|
+
const { results } = await runCLI(argv, [projectRoot]);
|
|
72
|
+
|
|
73
|
+
// ── 6. Return results for the caller to inspect ────────────
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a yargs-compatible argv object that runCLI understands.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} config - Merged Jest config.
|
|
81
|
+
* @param {object} extra
|
|
82
|
+
* @param {string[]} [extra.testFiles]
|
|
83
|
+
* @param {boolean} [extra.watch]
|
|
84
|
+
* @param {string} extra.projectRoot
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
function buildArgv(config, { testFiles, watch, projectRoot }) {
|
|
88
|
+
const argv = {
|
|
89
|
+
// Serialise the config so Jest uses our merged config
|
|
90
|
+
// instead of searching for jest.config.* files.
|
|
91
|
+
config: JSON.stringify(config),
|
|
92
|
+
|
|
93
|
+
// Project root for resolution
|
|
94
|
+
rootDir: projectRoot,
|
|
95
|
+
|
|
96
|
+
// Flags
|
|
97
|
+
watch: watch,
|
|
98
|
+
watchAll: false,
|
|
99
|
+
ci: process.env.CI === 'true',
|
|
100
|
+
|
|
101
|
+
// Pass-through values that CLI users might expect
|
|
102
|
+
verbose: config.verbose ?? true,
|
|
103
|
+
collectCoverage: config.collectCoverage ?? false,
|
|
104
|
+
passWithNoTests: config.passWithNoTests ?? true,
|
|
105
|
+
maxWorkers: config.maxWorkers ?? '50%',
|
|
106
|
+
silent: config.silent ?? false,
|
|
107
|
+
|
|
108
|
+
// Do not search for config files automatically
|
|
109
|
+
_: testFiles ?? [],
|
|
110
|
+
$0: 'unit-testing-framework',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return argv;
|
|
114
|
+
}
|