@vitronai/themis 0.1.0-beta.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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/benchmark-gate.json +10 -0
- package/bin/themis.js +8 -0
- package/docs/api.md +210 -0
- package/docs/publish.md +55 -0
- package/docs/release-policy.md +54 -0
- package/docs/schemas/agent-result.v1.json +277 -0
- package/docs/schemas/failures.v1.json +78 -0
- package/docs/vscode-extension.md +40 -0
- package/docs/why-themis.md +111 -0
- package/globals.d.ts +22 -0
- package/globals.js +1 -0
- package/index.d.ts +190 -0
- package/index.js +17 -0
- package/package.json +90 -0
- package/src/artifacts.js +207 -0
- package/src/assets/themisBg.png +0 -0
- package/src/assets/themisLogo.png +0 -0
- package/src/assets/themisReport.png +0 -0
- package/src/cli.js +395 -0
- package/src/config.js +52 -0
- package/src/discovery.js +34 -0
- package/src/environment.js +108 -0
- package/src/expect.js +175 -0
- package/src/init.js +22 -0
- package/src/module-loader.js +489 -0
- package/src/reporter.js +2141 -0
- package/src/runner.js +168 -0
- package/src/runtime.js +472 -0
- package/src/snapshots.js +90 -0
- package/src/stability.js +98 -0
- package/src/test-utils.js +201 -0
- package/src/verdict.js +71 -0
- package/src/watch.js +154 -0
- package/src/worker.js +26 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
const { loadConfig } = require('./config');
|
|
2
|
+
const { discoverTests } = require('./discovery');
|
|
3
|
+
const { runTests } = require('./runner');
|
|
4
|
+
const { printSpec, printJson, printAgent, printNext, writeHtmlReport } = require('./reporter');
|
|
5
|
+
const { runInit } = require('./init');
|
|
6
|
+
const { writeRunArtifacts, readFailedTestsArtifact } = require('./artifacts');
|
|
7
|
+
const { buildStabilityReport, hasStabilityBreaches } = require('./stability');
|
|
8
|
+
const { verdictReveal } = require('./verdict');
|
|
9
|
+
const { runWatchMode } = require('./watch');
|
|
10
|
+
const { version: THEMIS_VERSION } = require('../package.json');
|
|
11
|
+
const SUPPORTED_REPORTERS = new Set(['spec', 'next', 'json', 'agent', 'html']);
|
|
12
|
+
const SUPPORTED_LEXICONS = new Set(['classic', 'themis']);
|
|
13
|
+
const SUPPORTED_ENVIRONMENTS = new Set(['node', 'jsdom']);
|
|
14
|
+
|
|
15
|
+
async function main(argv) {
|
|
16
|
+
const command = argv[0] || 'test';
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
if (command === 'init') {
|
|
20
|
+
runInit(cwd);
|
|
21
|
+
console.log('Themis initialized. Run: npx themis test');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (command !== 'test') {
|
|
26
|
+
printUsage();
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const config = loadConfig(cwd);
|
|
32
|
+
const flags = parseFlags(argv.slice(1));
|
|
33
|
+
|
|
34
|
+
if (flags.match) {
|
|
35
|
+
validateRegex(flags.match);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const reporter = resolveReporter(flags, config);
|
|
39
|
+
validateReporter(reporter);
|
|
40
|
+
const lexicon = resolveLexicon(flags);
|
|
41
|
+
validateLexicon(lexicon);
|
|
42
|
+
validateWorkerCount(flags.workers, config.maxWorkers);
|
|
43
|
+
validateStabilityRuns(flags.stability);
|
|
44
|
+
const environment = resolveEnvironment(flags, config);
|
|
45
|
+
validateEnvironment(environment, flags.environment, config.environment);
|
|
46
|
+
if (flags.watch) {
|
|
47
|
+
await runWatchMode({
|
|
48
|
+
cwd,
|
|
49
|
+
cliArgs: argv.slice(1)
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
printBanner(reporter);
|
|
54
|
+
const maxWorkers = resolveWorkerCount(flags.workers, config.maxWorkers);
|
|
55
|
+
const stabilityRuns = resolveStabilityRuns(flags.stability);
|
|
56
|
+
|
|
57
|
+
let files = discoverTests(cwd, config);
|
|
58
|
+
if (files.length === 0) {
|
|
59
|
+
console.log(`No test files found in ${config.testDir}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let allowedFullNames = null;
|
|
64
|
+
if (flags.rerunFailed) {
|
|
65
|
+
const artifact = readFailedTestsArtifact(cwd);
|
|
66
|
+
if (artifact && artifact.parseError) {
|
|
67
|
+
console.log(
|
|
68
|
+
`Failed to parse failed test artifact at ${artifact.failuresPath}: ${artifact.parseError}. Run a full test pass to regenerate it.`
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!artifact || artifact.failedTests.length === 0) {
|
|
74
|
+
console.log('No failed test artifact found. Run a failing test first.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fileSet = new Set(artifact.failedTests.map((entry) => entry.file));
|
|
79
|
+
allowedFullNames = artifact.failedTests.map((entry) => entry.fullName);
|
|
80
|
+
files = files.filter((file) => fileSet.has(file));
|
|
81
|
+
|
|
82
|
+
if (files.length === 0) {
|
|
83
|
+
console.log('No matching files found for failed test artifact.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const runResults = [];
|
|
89
|
+
for (let i = 0; i < stabilityRuns; i += 1) {
|
|
90
|
+
const runResult = await runTests(files, {
|
|
91
|
+
maxWorkers,
|
|
92
|
+
match: flags.match || null,
|
|
93
|
+
allowedFullNames,
|
|
94
|
+
noMemes: Boolean(flags.noMemes),
|
|
95
|
+
cwd,
|
|
96
|
+
environment,
|
|
97
|
+
setupFiles: config.setupFiles,
|
|
98
|
+
tsconfigPath: config.tsconfigPath,
|
|
99
|
+
updateSnapshots: Boolean(flags.updateSnapshots)
|
|
100
|
+
});
|
|
101
|
+
runResults.push(runResult);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = runResults[runResults.length - 1];
|
|
105
|
+
const stabilityReport = stabilityRuns > 1 ? buildStabilityReport(runResults) : null;
|
|
106
|
+
if (stabilityReport) {
|
|
107
|
+
result.stability = stabilityReport;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
writeRunArtifacts(cwd, result);
|
|
111
|
+
printResult(reporter, result, {
|
|
112
|
+
lexicon,
|
|
113
|
+
cwd,
|
|
114
|
+
htmlOutput: flags.htmlOutput || null
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const revealFailed = result.summary.failed > 0 || hasStabilityBreaches(stabilityReport);
|
|
118
|
+
await maybeRevealVerdict(reporter, result, stabilityReport, revealFailed);
|
|
119
|
+
|
|
120
|
+
if (revealFailed) {
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveReporter(flags, config) {
|
|
126
|
+
if (flags.reporter) {
|
|
127
|
+
return flags.reporter;
|
|
128
|
+
}
|
|
129
|
+
if (flags.next) {
|
|
130
|
+
return 'next';
|
|
131
|
+
}
|
|
132
|
+
if (flags.agent) {
|
|
133
|
+
return 'agent';
|
|
134
|
+
}
|
|
135
|
+
if (flags.json) {
|
|
136
|
+
return 'json';
|
|
137
|
+
}
|
|
138
|
+
return config.reporter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveLexicon(flags) {
|
|
142
|
+
return flags.lexicon || 'classic';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveEnvironment(flags, config) {
|
|
146
|
+
return flags.environment || config.environment || 'node';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printResult(reporter, result, options = {}) {
|
|
150
|
+
if (reporter === 'json') {
|
|
151
|
+
printJson(result);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (reporter === 'agent') {
|
|
155
|
+
printAgent(result);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (reporter === 'next') {
|
|
159
|
+
printNext(result, options);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (reporter === 'html') {
|
|
163
|
+
const reportPath = writeHtmlReport(result, {
|
|
164
|
+
cwd: options.cwd,
|
|
165
|
+
outputPath: options.htmlOutput,
|
|
166
|
+
lexicon: options.lexicon
|
|
167
|
+
});
|
|
168
|
+
console.log(`HTML report written to ${reportPath}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
printSpec(result, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseFlags(args) {
|
|
175
|
+
const flags = {};
|
|
176
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
177
|
+
const token = args[i];
|
|
178
|
+
if (token === '--json') {
|
|
179
|
+
flags.json = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (token === '--agent') {
|
|
183
|
+
flags.agent = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (token === '--next') {
|
|
187
|
+
flags.next = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (token === '--rerun-failed') {
|
|
191
|
+
flags.rerunFailed = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (token === '--no-memes') {
|
|
195
|
+
flags.noMemes = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (token === '-w' || token === '--watch') {
|
|
199
|
+
flags.watch = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (token === '-u' || token === '--update-snapshots') {
|
|
203
|
+
flags.updateSnapshots = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (token === '--reporter') {
|
|
207
|
+
flags.reporter = requireFlagValue(args, i, '--reporter');
|
|
208
|
+
i += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (token === '--lexicon') {
|
|
212
|
+
flags.lexicon = requireFlagValue(args, i, '--lexicon');
|
|
213
|
+
i += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (token === '--workers') {
|
|
217
|
+
flags.workers = requireFlagValue(args, i, '--workers');
|
|
218
|
+
i += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (token === '--environment') {
|
|
222
|
+
flags.environment = requireFlagValue(args, i, '--environment');
|
|
223
|
+
i += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (token === '--html-output') {
|
|
227
|
+
flags.htmlOutput = requireFlagValue(args, i, '--html-output');
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (token === '--stability') {
|
|
232
|
+
flags.stability = requireFlagValue(args, i, '--stability');
|
|
233
|
+
i += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (token === '--match') {
|
|
237
|
+
flags.match = requireFlagValue(args, i, '--match');
|
|
238
|
+
i += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return flags;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateRegex(pattern) {
|
|
246
|
+
try {
|
|
247
|
+
new RegExp(pattern);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new Error(`Invalid --match regex: ${pattern}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function requireFlagValue(args, index, flag) {
|
|
254
|
+
const value = args[index + 1];
|
|
255
|
+
if (typeof value !== 'string' || value.startsWith('--')) {
|
|
256
|
+
throw new Error(`Missing value for ${flag}`);
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validateReporter(reporter) {
|
|
262
|
+
if (SUPPORTED_REPORTERS.has(reporter)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Unsupported reporter: ${reporter}. Use one of: spec, next, json, agent, html.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function validateLexicon(lexicon) {
|
|
269
|
+
if (SUPPORTED_LEXICONS.has(lexicon)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Unsupported --lexicon value: ${lexicon}. Use one of: classic, themis.`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateEnvironment(environment, flagValue, configValue) {
|
|
276
|
+
if (SUPPORTED_ENVIRONMENTS.has(environment)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (flagValue !== undefined) {
|
|
281
|
+
throw new Error(`Unsupported --environment value: ${flagValue}. Use one of: node, jsdom.`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Unsupported config environment value: ${String(configValue)}. Use one of: node, jsdom.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function validateWorkerCount(flagValue, configValue) {
|
|
290
|
+
const sourceValue = flagValue !== undefined ? flagValue : configValue;
|
|
291
|
+
const parsed = Number(sourceValue);
|
|
292
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (flagValue !== undefined) {
|
|
297
|
+
throw new Error(`Invalid --workers value: ${flagValue}. Use a positive integer.`);
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`Invalid config maxWorkers value: ${String(configValue)}. Use a positive integer.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveWorkerCount(flagValue, configValue) {
|
|
303
|
+
const sourceValue = flagValue !== undefined ? flagValue : configValue;
|
|
304
|
+
return Number(sourceValue);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function validateStabilityRuns(value) {
|
|
308
|
+
if (value === undefined) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const parsed = Number(value);
|
|
312
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Invalid --stability value: ${value}. Use a positive integer.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolveStabilityRuns(value) {
|
|
319
|
+
if (value === undefined) {
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
return Number(value);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printUsage() {
|
|
326
|
+
console.log('Usage: themis <command> [options]');
|
|
327
|
+
console.log('Commands:');
|
|
328
|
+
console.log(' init Create themis.config.json and sample tests');
|
|
329
|
+
console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [-w|--watch] [-u|--update-snapshots] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function printBanner(reporter) {
|
|
333
|
+
if (reporter === 'json' || reporter === 'agent') {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const displayVersion = normalizeDisplayVersion(THEMIS_VERSION);
|
|
338
|
+
const lines = [
|
|
339
|
+
'══════════════════════════════════════════════════════',
|
|
340
|
+
` ⚖️ THEMIS v${displayVersion}`,
|
|
341
|
+
' AI UNIT TEST FRAMEWORK',
|
|
342
|
+
' AI’S VERDICT ENGINE',
|
|
343
|
+
'══════════════════════════════════════════════════════',
|
|
344
|
+
'',
|
|
345
|
+
' ████████╗██╗ ██╗███████╗███╗ ███╗██╗███████╗',
|
|
346
|
+
' ╚══██╔══╝██║ ██║██╔════╝████╗ ████║██║██╔════╝',
|
|
347
|
+
' ██║ ███████║█████╗ ██╔████╔██║██║███████╗',
|
|
348
|
+
' ██║ ██╔══██║██╔══╝ ██║╚██╔╝██║██║╚════██║',
|
|
349
|
+
' ██║ ██║ ██║███████╗██║ ╚═╝ ██║██║███████║',
|
|
350
|
+
' ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝╚══════╝',
|
|
351
|
+
'',
|
|
352
|
+
'══════════════════════════════════════════════════════'
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
console.log(lines.join('\n'));
|
|
356
|
+
console.log('');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeDisplayVersion(version) {
|
|
360
|
+
const value = String(version || '').trim();
|
|
361
|
+
const match = value.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
362
|
+
if (!match) {
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function maybeRevealVerdict(reporter, result, stabilityReport, failed) {
|
|
369
|
+
if (reporter !== 'next' && reporter !== 'spec') {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const unstable = Number(stabilityReport?.summary?.unstable || 0);
|
|
374
|
+
const stableFail = Number(stabilityReport?.summary?.stableFail || 0);
|
|
375
|
+
let detail = 'TRUTH UPHELD';
|
|
376
|
+
|
|
377
|
+
if (failed) {
|
|
378
|
+
if (unstable > 0) {
|
|
379
|
+
detail = `UNSTABLE SIGNAL (${unstable})`;
|
|
380
|
+
} else if (stableFail > 0) {
|
|
381
|
+
detail = `STABLE FAILURES (${stableFail})`;
|
|
382
|
+
} else {
|
|
383
|
+
detail = `${result.summary.failed} FAILURE${result.summary.failed === 1 ? '' : 'S'} DETECTED`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
await verdictReveal({
|
|
388
|
+
ok: !failed,
|
|
389
|
+
title: 'THEMIS VERDICT',
|
|
390
|
+
detail,
|
|
391
|
+
delayMs: process.env.THEMIS_REVEAL_DELAY_MS
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = { main };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
testDir: 'tests',
|
|
7
|
+
testRegex: '\\.(test|spec)\\.(js|jsx|ts|tsx)$',
|
|
8
|
+
maxWorkers: Math.max(1, os.cpus().length - 1),
|
|
9
|
+
reporter: 'next',
|
|
10
|
+
environment: 'node',
|
|
11
|
+
setupFiles: [],
|
|
12
|
+
tsconfigPath: 'tsconfig.json'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function loadConfig(cwd) {
|
|
16
|
+
const configPath = path.join(cwd, 'themis.config.json');
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return { ...DEFAULT_CONFIG, setupFiles: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return normalizeConfig({ ...DEFAULT_CONFIG, ...parsed });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function initConfig(cwd) {
|
|
27
|
+
const configPath = path.join(cwd, 'themis.config.json');
|
|
28
|
+
if (!fs.existsSync(configPath)) {
|
|
29
|
+
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeConfig(config) {
|
|
34
|
+
if (!Array.isArray(config.setupFiles) || !config.setupFiles.every((entry) => typeof entry === 'string')) {
|
|
35
|
+
throw new Error('Invalid config setupFiles value: expected an array of file paths.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (config.tsconfigPath !== null && typeof config.tsconfigPath !== 'string') {
|
|
39
|
+
throw new Error('Invalid config tsconfigPath value: expected a string path or null.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
setupFiles: [...config.setupFiles]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
DEFAULT_CONFIG,
|
|
50
|
+
loadConfig,
|
|
51
|
+
initConfig
|
|
52
|
+
};
|
package/src/discovery.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function discoverTests(cwd, config) {
|
|
5
|
+
const start = path.resolve(cwd, config.testDir);
|
|
6
|
+
const regex = new RegExp(config.testRegex);
|
|
7
|
+
const files = [];
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(start)) {
|
|
10
|
+
return files;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
walk(start, regex, files);
|
|
14
|
+
return files.sort();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walk(dir, regex, files) {
|
|
18
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
walk(fullPath, regex, files);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (entry.isFile() && regex.test(entry.name)) {
|
|
29
|
+
files.push(fullPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { discoverTests };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function installTestEnvironment(name = 'node') {
|
|
2
|
+
if (name === 'node') {
|
|
3
|
+
return {
|
|
4
|
+
name,
|
|
5
|
+
teardown() {}
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (name === 'jsdom') {
|
|
10
|
+
return installJsdomEnvironment();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
throw new Error(`Unsupported test environment: ${String(name)}. Use one of: node, jsdom.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function installJsdomEnvironment() {
|
|
17
|
+
let JSDOM;
|
|
18
|
+
try {
|
|
19
|
+
({ JSDOM } = require('jsdom'));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"The 'jsdom' package is required for the jsdom environment. Install with: npm i jsdom"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
27
|
+
url: 'http://localhost/',
|
|
28
|
+
pretendToBeVisual: true
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const previousDescriptors = new Map();
|
|
32
|
+
const install = (key, value) => {
|
|
33
|
+
previousDescriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key) || null);
|
|
34
|
+
Object.defineProperty(globalThis, key, {
|
|
35
|
+
configurable: true,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const explicitGlobals = {
|
|
43
|
+
window: dom.window,
|
|
44
|
+
self: dom.window,
|
|
45
|
+
document: dom.window.document,
|
|
46
|
+
navigator: dom.window.navigator,
|
|
47
|
+
location: dom.window.location,
|
|
48
|
+
history: dom.window.history,
|
|
49
|
+
localStorage: dom.window.localStorage,
|
|
50
|
+
sessionStorage: dom.window.sessionStorage,
|
|
51
|
+
Node: dom.window.Node,
|
|
52
|
+
Element: dom.window.Element,
|
|
53
|
+
HTMLElement: dom.window.HTMLElement,
|
|
54
|
+
DocumentFragment: dom.window.DocumentFragment,
|
|
55
|
+
Event: dom.window.Event,
|
|
56
|
+
CustomEvent: dom.window.CustomEvent,
|
|
57
|
+
EventTarget: dom.window.EventTarget,
|
|
58
|
+
MouseEvent: dom.window.MouseEvent,
|
|
59
|
+
KeyboardEvent: dom.window.KeyboardEvent,
|
|
60
|
+
DOMParser: dom.window.DOMParser,
|
|
61
|
+
MutationObserver: dom.window.MutationObserver,
|
|
62
|
+
getComputedStyle: dom.window.getComputedStyle.bind(dom.window),
|
|
63
|
+
requestAnimationFrame: dom.window.requestAnimationFrame.bind(dom.window),
|
|
64
|
+
cancelAnimationFrame: dom.window.cancelAnimationFrame.bind(dom.window)
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (const [key, value] of Object.entries(explicitGlobals)) {
|
|
68
|
+
install(key, value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
|
72
|
+
if (key in globalThis || key === 'undefined') {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
install(key, dom.window[key]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
install('IS_REACT_ACT_ENVIRONMENT', true);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: 'jsdom',
|
|
82
|
+
beforeEach() {
|
|
83
|
+
dom.window.document.head.innerHTML = '';
|
|
84
|
+
dom.window.document.body.innerHTML = '';
|
|
85
|
+
},
|
|
86
|
+
afterEach() {
|
|
87
|
+
if (typeof globalThis.cleanup === 'function') {
|
|
88
|
+
globalThis.cleanup();
|
|
89
|
+
}
|
|
90
|
+
dom.window.document.head.innerHTML = '';
|
|
91
|
+
dom.window.document.body.innerHTML = '';
|
|
92
|
+
},
|
|
93
|
+
teardown() {
|
|
94
|
+
for (const [key, descriptor] of previousDescriptors.entries()) {
|
|
95
|
+
if (descriptor) {
|
|
96
|
+
Object.defineProperty(globalThis, key, descriptor);
|
|
97
|
+
} else {
|
|
98
|
+
delete globalThis[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
dom.window.close();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
installTestEnvironment
|
|
108
|
+
};
|