@remix-run/test 0.0.0 → 0.2.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/LICENSE +21 -0
- package/README.md +430 -2
- package/dist/app/client/entry.d.ts +2 -0
- package/dist/app/client/entry.d.ts.map +1 -0
- package/dist/app/client/entry.js +324 -0
- package/dist/app/client/iframe.d.ts +2 -0
- package/dist/app/client/iframe.d.ts.map +1 -0
- package/dist/app/client/iframe.js +22 -0
- package/dist/app/server.d.ts +6 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +303 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +14 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +305 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/colors.d.ts +2 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +2 -0
- package/dist/lib/config.d.ts +91 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +255 -0
- package/dist/lib/context.d.ts +93 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +65 -0
- package/dist/lib/coverage-loader.d.ts +16 -0
- package/dist/lib/coverage-loader.d.ts.map +1 -0
- package/dist/lib/coverage-loader.js +20 -0
- package/dist/lib/coverage.d.ts +28 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +212 -0
- package/dist/lib/executor.d.ts +4 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +128 -0
- package/dist/lib/fake-timers.d.ts +6 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +45 -0
- package/dist/lib/framework.d.ts +107 -0
- package/dist/lib/framework.d.ts.map +1 -0
- package/dist/lib/framework.js +198 -0
- package/dist/lib/import-module.d.ts +2 -0
- package/dist/lib/import-module.d.ts.map +1 -0
- package/dist/lib/import-module.js +29 -0
- package/dist/lib/mock.d.ts +52 -0
- package/dist/lib/mock.d.ts.map +1 -0
- package/dist/lib/mock.js +61 -0
- package/dist/lib/normalize.d.ts +2 -0
- package/dist/lib/normalize.d.ts.map +1 -0
- package/dist/lib/normalize.js +18 -0
- package/dist/lib/playwright.d.ts +15 -0
- package/dist/lib/playwright.d.ts.map +1 -0
- package/dist/lib/playwright.js +81 -0
- package/dist/lib/reporters/dot.d.ts +9 -0
- package/dist/lib/reporters/dot.d.ts.map +1 -0
- package/dist/lib/reporters/dot.js +56 -0
- package/dist/lib/reporters/files.d.ts +9 -0
- package/dist/lib/reporters/files.d.ts.map +1 -0
- package/dist/lib/reporters/files.js +71 -0
- package/dist/lib/reporters/index.d.ts +13 -0
- package/dist/lib/reporters/index.d.ts.map +1 -0
- package/dist/lib/reporters/index.js +18 -0
- package/dist/lib/reporters/results.d.ts +30 -0
- package/dist/lib/reporters/results.d.ts.map +1 -0
- package/dist/lib/reporters/results.js +1 -0
- package/dist/lib/reporters/spec.d.ts +9 -0
- package/dist/lib/reporters/spec.d.ts.map +1 -0
- package/dist/lib/reporters/spec.js +153 -0
- package/dist/lib/reporters/tap.d.ts +9 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -0
- package/dist/lib/reporters/tap.js +54 -0
- package/dist/lib/runner-browser.d.ts +21 -0
- package/dist/lib/runner-browser.d.ts.map +1 -0
- package/dist/lib/runner-browser.js +117 -0
- package/dist/lib/runner.d.ts +14 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +118 -0
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +2 -0
- package/dist/lib/ts-transform.d.ts +4 -0
- package/dist/lib/ts-transform.d.ts.map +1 -0
- package/dist/lib/ts-transform.js +29 -0
- package/dist/lib/watcher.d.ts +5 -0
- package/dist/lib/watcher.d.ts.map +1 -0
- package/dist/lib/watcher.js +39 -0
- package/dist/lib/worker-e2e.d.ts +2 -0
- package/dist/lib/worker-e2e.d.ts.map +1 -0
- package/dist/lib/worker-e2e.js +49 -0
- package/dist/lib/worker.d.ts +2 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +57 -0
- package/dist/test/coverage/fixture.d.ts +5 -0
- package/dist/test/coverage/fixture.d.ts.map +1 -0
- package/dist/test/coverage/fixture.js +32 -0
- package/dist/test/coverage/test-browser.d.ts +2 -0
- package/dist/test/coverage/test-browser.d.ts.map +1 -0
- package/dist/test/coverage/test-browser.js +24 -0
- package/dist/test/coverage/test-e2e.d.ts +2 -0
- package/dist/test/coverage/test-e2e.d.ts.map +1 -0
- package/dist/test/coverage/test-e2e.js +60 -0
- package/dist/test/coverage/test-unit.d.ts +2 -0
- package/dist/test/coverage/test-unit.d.ts.map +1 -0
- package/dist/test/coverage/test-unit.js +27 -0
- package/dist/test/framework.test.browser.d.ts +2 -0
- package/dist/test/framework.test.browser.d.ts.map +1 -0
- package/dist/test/framework.test.browser.js +107 -0
- package/dist/test/framework.test.e2e.d.ts +2 -0
- package/dist/test/framework.test.e2e.d.ts.map +1 -0
- package/dist/test/framework.test.e2e.js +34 -0
- package/package.json +79 -5
- package/src/app/client/entry.ts +353 -0
- package/src/app/client/iframe.ts +18 -0
- package/src/app/server.ts +336 -0
- package/src/cli-entry.ts +15 -0
- package/src/cli.ts +384 -0
- package/src/index.ts +16 -0
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +377 -0
- package/src/lib/context.ts +168 -0
- package/src/lib/coverage-loader.ts +31 -0
- package/src/lib/coverage.ts +320 -0
- package/src/lib/executor.ts +145 -0
- package/src/lib/fake-timers.ts +64 -0
- package/src/lib/framework.ts +251 -0
- package/src/lib/import-module.ts +29 -0
- package/src/lib/mock.ts +89 -0
- package/src/lib/normalize.ts +22 -0
- package/src/lib/playwright.ts +100 -0
- package/src/lib/reporters/dot.ts +58 -0
- package/src/lib/reporters/files.ts +77 -0
- package/src/lib/reporters/index.ts +27 -0
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +174 -0
- package/src/lib/reporters/tap.ts +58 -0
- package/src/lib/runner-browser.ts +165 -0
- package/src/lib/runner.ts +189 -0
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/watcher.ts +46 -0
- package/src/lib/worker-e2e.ts +54 -0
- package/src/lib/worker.ts +50 -0
- package/src/test/coverage/fixture.ts +34 -0
- package/src/test/coverage/test-browser.ts +29 -0
- package/src/test/coverage/test-e2e.ts +70 -0
- package/src/test/coverage/test-unit.ts +32 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as fsp from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { colors } from "./colors.js";
|
|
6
|
+
import { transformTypeScript } from "./ts-transform.js";
|
|
7
|
+
// Istanbul packages are loaded lazily so that FORCE_COLOR can be set based on
|
|
8
|
+
// the actual TTY state before supports-color caches its detection result.
|
|
9
|
+
let _istanbul;
|
|
10
|
+
function getIstanbul() {
|
|
11
|
+
if (!_istanbul) {
|
|
12
|
+
process.env.FORCE_COLOR ??= process.stdout.isTTY ? '1' : '0';
|
|
13
|
+
let require = createRequire(import.meta.url);
|
|
14
|
+
_istanbul = {
|
|
15
|
+
V8ToIstanbul: require('v8-to-istanbul'),
|
|
16
|
+
createCoverageMap: require('istanbul-lib-coverage').createCoverageMap,
|
|
17
|
+
createContext: require('istanbul-lib-report')
|
|
18
|
+
.createContext,
|
|
19
|
+
reports: require('istanbul-reports'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return _istanbul;
|
|
23
|
+
}
|
|
24
|
+
function matchesGlobs(filePath, globs) {
|
|
25
|
+
return globs.some((glob) => path.matchesGlob(filePath, glob));
|
|
26
|
+
}
|
|
27
|
+
function filterCoverageMap(coverageMap, cwd, config) {
|
|
28
|
+
let filtered = getIstanbul().createCoverageMap({});
|
|
29
|
+
for (let filePath of coverageMap.files()) {
|
|
30
|
+
let relative = path.relative(cwd, filePath);
|
|
31
|
+
if (config.include && config.include.length > 0) {
|
|
32
|
+
if (!matchesGlobs(relative, config.include))
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (config.exclude && config.exclude.length > 0) {
|
|
36
|
+
if (matchesGlobs(relative, config.exclude))
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let fc = coverageMap.fileCoverageFor(filePath);
|
|
40
|
+
filtered.addFileCoverage({ ...fc.toJSON(), path: relative });
|
|
41
|
+
}
|
|
42
|
+
return filtered;
|
|
43
|
+
}
|
|
44
|
+
function checkThresholds(coverageMap, config) {
|
|
45
|
+
let { statements, lines, branches, functions } = config;
|
|
46
|
+
if (statements === undefined &&
|
|
47
|
+
lines === undefined &&
|
|
48
|
+
branches === undefined &&
|
|
49
|
+
functions === undefined)
|
|
50
|
+
return true;
|
|
51
|
+
let summary = coverageMap.getCoverageSummary();
|
|
52
|
+
let passed = true;
|
|
53
|
+
if (statements !== undefined) {
|
|
54
|
+
let pct = summary.statements.pct;
|
|
55
|
+
if (pct < statements) {
|
|
56
|
+
console.error(colors.red(`\nError: Coverage threshold not met (statements ${pct.toFixed(2)}% < ${statements}%)`));
|
|
57
|
+
passed = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (lines !== undefined) {
|
|
61
|
+
let pct = summary.lines.pct;
|
|
62
|
+
if (pct < lines) {
|
|
63
|
+
console.error(colors.red(`\nError: Coverage threshold not met (lines ${pct.toFixed(2)}% < ${lines}%)`));
|
|
64
|
+
passed = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (branches !== undefined) {
|
|
68
|
+
let pct = summary.branches.pct;
|
|
69
|
+
if (pct < branches) {
|
|
70
|
+
console.error(colors.red(`\nError: Coverage threshold not met (branches ${pct.toFixed(2)}% < ${branches}%)`));
|
|
71
|
+
passed = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (functions !== undefined) {
|
|
75
|
+
let pct = summary.functions.pct;
|
|
76
|
+
if (pct < functions) {
|
|
77
|
+
console.error(colors.red(`\nError: Coverage threshold not met (functions ${pct.toFixed(2)}% < ${functions}%)`));
|
|
78
|
+
passed = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return passed;
|
|
82
|
+
}
|
|
83
|
+
async function writeIstanbulReports(coverageMap, cwd, outDir) {
|
|
84
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
85
|
+
let { createContext, reports } = getIstanbul();
|
|
86
|
+
let ctx = createContext({ coverageMap, dir: outDir });
|
|
87
|
+
console.log('\nCoverage report:');
|
|
88
|
+
reports.create('text').execute(ctx);
|
|
89
|
+
reports.create('lcovonly').execute(ctx);
|
|
90
|
+
console.log(`\nLCOV coverage written to ${path.relative(cwd, path.join(outDir, 'lcov.info'))}`);
|
|
91
|
+
}
|
|
92
|
+
// Convert a single V8 coverage entry to Istanbul format and merge it into the
|
|
93
|
+
// coverage map.
|
|
94
|
+
//
|
|
95
|
+
// V8 reports byte offsets against the JS bytes it actually instrumented. When
|
|
96
|
+
// the entry already carries that source (Playwright's `coverage.stopJSCoverage`
|
|
97
|
+
// returns it on each entry, including the inline source map), we hand it
|
|
98
|
+
// straight to v8-to-istanbul so the offsets line up exactly. The server path
|
|
99
|
+
// uses Node's `NODE_V8_COVERAGE` JSON, which doesn't include source — there we
|
|
100
|
+
// re-derive by re-running our esbuild transform on the original TS file.
|
|
101
|
+
async function addV8EntryToCoverageMap(coverageMap, filePath, functions, source) {
|
|
102
|
+
let { V8ToIstanbul } = getIstanbul();
|
|
103
|
+
let converter = new V8ToIstanbul(filePath, undefined, { source });
|
|
104
|
+
await converter.load();
|
|
105
|
+
converter.applyCoverage(functions);
|
|
106
|
+
coverageMap.merge(converter.toIstanbul());
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function shouldExcludeFromCoverage(filePath, rootDir, testFiles) {
|
|
110
|
+
return (!filePath.startsWith(rootDir + path.sep) ||
|
|
111
|
+
filePath.includes(`${path.sep}node_modules${path.sep}`) ||
|
|
112
|
+
testFiles.has(filePath));
|
|
113
|
+
}
|
|
114
|
+
export async function collectServerCoverageMap(coverageDataDir, cwd, testFiles) {
|
|
115
|
+
let { createCoverageMap } = getIstanbul();
|
|
116
|
+
let coverageMap = createCoverageMap({});
|
|
117
|
+
let converted = 0;
|
|
118
|
+
let files;
|
|
119
|
+
try {
|
|
120
|
+
files = (await fsp.readdir(coverageDataDir)).filter((f) => f.startsWith('coverage-') && f.endsWith('.json'));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
for (let file of files) {
|
|
126
|
+
let data = JSON.parse(await fsp.readFile(path.join(coverageDataDir, file), 'utf-8'));
|
|
127
|
+
let scriptCoverages = data.result ?? [];
|
|
128
|
+
for (let entry of scriptCoverages) {
|
|
129
|
+
if (!entry.url.startsWith('file://'))
|
|
130
|
+
continue;
|
|
131
|
+
let filePath;
|
|
132
|
+
try {
|
|
133
|
+
filePath = fileURLToPath(entry.url);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!filePath ||
|
|
139
|
+
!['.ts', '.tsx'].includes(path.extname(filePath)) ||
|
|
140
|
+
shouldExcludeFromCoverage(filePath, cwd, testFiles)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
// For server unit tests, we transform the TS with a module loader and V8 tracks
|
|
145
|
+
// coverage using byte offsets from the transformed JS. Re-transform with the
|
|
146
|
+
// same `esbuild` call here so offsets align, then pass the result with its
|
|
147
|
+
// inline source map to v8-to-istanbul.
|
|
148
|
+
let tsSource = await fsp.readFile(filePath, 'utf-8');
|
|
149
|
+
let { code } = await transformTypeScript(tsSource, filePath);
|
|
150
|
+
let success = await addV8EntryToCoverageMap(coverageMap, filePath, entry.functions, code);
|
|
151
|
+
if (success)
|
|
152
|
+
converted++;
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// Skip files that can't be converted
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Clean up raw V8 coverage JSON files now that we've processed them
|
|
160
|
+
//await Promise.all(files.map((f) => fsp.rm(path.join(coverageDataDir, f), { force: true })))
|
|
161
|
+
return converted > 0 ? coverageMap : null;
|
|
162
|
+
}
|
|
163
|
+
export async function collectCoverageMapFromPlaywright(entries, rootDir, testFiles, resolveRelativePath) {
|
|
164
|
+
let { createCoverageMap } = getIstanbul();
|
|
165
|
+
let coverageMap = createCoverageMap({});
|
|
166
|
+
let converted = 0;
|
|
167
|
+
for (let entry of entries) {
|
|
168
|
+
let filePath;
|
|
169
|
+
try {
|
|
170
|
+
let relativePath = await resolveRelativePath(new URL(entry.url).pathname);
|
|
171
|
+
if (!relativePath)
|
|
172
|
+
continue;
|
|
173
|
+
// Ignore entries outside the root dir, entries in node_modules, and test files
|
|
174
|
+
filePath = path.resolve(rootDir, relativePath);
|
|
175
|
+
if (shouldExcludeFromCoverage(filePath, rootDir, testFiles)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Ensure file exists
|
|
179
|
+
await fsp.access(filePath);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!entry.source) {
|
|
185
|
+
throw new Error(`Entry for ${entry.url} is missing source, cannot convert coverage. Ensure the browser launched with Playwright's JS coverage enabled.`);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
let success = await addV8EntryToCoverageMap(coverageMap, filePath, entry.functions, entry.source);
|
|
189
|
+
if (success)
|
|
190
|
+
converted++;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Skip files that can't be converted
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return converted > 0 ? coverageMap : null;
|
|
197
|
+
}
|
|
198
|
+
export async function generateCombinedCoverageReport(maps, cwd, config) {
|
|
199
|
+
let { createCoverageMap } = getIstanbul();
|
|
200
|
+
let combined = createCoverageMap({});
|
|
201
|
+
for (let map of maps) {
|
|
202
|
+
if (map)
|
|
203
|
+
combined.merge(map);
|
|
204
|
+
}
|
|
205
|
+
if (combined.files().length === 0) {
|
|
206
|
+
console.log('No coverage data collected.');
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
let filtered = filterCoverageMap(combined, cwd, config);
|
|
210
|
+
await writeIstanbulReports(filtered, cwd, config.dir);
|
|
211
|
+
return checkThresholds(filtered, config);
|
|
212
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type CreateTestContextOptions } from './context.ts';
|
|
2
|
+
import type { TestResults } from './reporters/results.ts';
|
|
3
|
+
export declare function runTests(options?: Omit<CreateTestContextOptions, 'addE2ECoverageEntries'>): Promise<TestResults>;
|
|
4
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/lib/executor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAE/E,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAErE,wBAAsB,QAAQ,CAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,uBAAuB,CAAC,GAChE,OAAO,CAAC,WAAW,CAAC,CA0ItB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createTestContext } from "./context.js";
|
|
2
|
+
export async function runTests(options) {
|
|
3
|
+
let suites = globalThis.__testSuites || [];
|
|
4
|
+
let e2eCoverageEntries = [];
|
|
5
|
+
let results = {
|
|
6
|
+
passed: 0,
|
|
7
|
+
failed: 0,
|
|
8
|
+
skipped: 0,
|
|
9
|
+
todo: 0,
|
|
10
|
+
tests: [],
|
|
11
|
+
};
|
|
12
|
+
let hasOnlySuites = suites.some((s) => s.only);
|
|
13
|
+
for (let suite of suites) {
|
|
14
|
+
// If any suite uses .only, skip all non-only suites
|
|
15
|
+
if (hasOnlySuites && !suite.only) {
|
|
16
|
+
for (let test of suite.tests) {
|
|
17
|
+
results.tests.push({
|
|
18
|
+
name: test.name,
|
|
19
|
+
suiteName: suite.name,
|
|
20
|
+
status: 'skipped',
|
|
21
|
+
duration: 0,
|
|
22
|
+
});
|
|
23
|
+
results.skipped++;
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (suite.skip || suite.todo) {
|
|
28
|
+
let status = suite.todo ? 'todo' : 'skipped';
|
|
29
|
+
for (let test of suite.tests) {
|
|
30
|
+
results.tests.push({ name: test.name, suiteName: suite.name, status, duration: 0 });
|
|
31
|
+
results[status]++;
|
|
32
|
+
}
|
|
33
|
+
// describe.todo('name') with no tests — add placeholder so suite appears in output
|
|
34
|
+
if (suite.tests.length === 0) {
|
|
35
|
+
results.tests.push({ name: '', suiteName: suite.name, status, duration: 0 });
|
|
36
|
+
results[status]++;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (suite.beforeAll) {
|
|
41
|
+
try {
|
|
42
|
+
await suite.beforeAll();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`beforeAll failed in suite "${suite.name}":`, error);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let hasOnlyTests = suite.tests.some((t) => t.only);
|
|
50
|
+
for (let test of suite.tests) {
|
|
51
|
+
// If any test uses .only, skip all non-only tests in this suite
|
|
52
|
+
if (hasOnlyTests && !test.only) {
|
|
53
|
+
results.tests.push({
|
|
54
|
+
name: test.name,
|
|
55
|
+
suiteName: suite.name,
|
|
56
|
+
status: 'skipped',
|
|
57
|
+
duration: 0,
|
|
58
|
+
});
|
|
59
|
+
results.skipped++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (test.skip || test.todo) {
|
|
63
|
+
let status = test.todo ? 'todo' : 'skipped';
|
|
64
|
+
results.tests.push({ name: test.name, suiteName: suite.name, status, duration: 0 });
|
|
65
|
+
results[status]++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let startTime = performance.now();
|
|
69
|
+
let result = {
|
|
70
|
+
name: test.name,
|
|
71
|
+
suiteName: suite.name,
|
|
72
|
+
status: 'passed',
|
|
73
|
+
duration: 0,
|
|
74
|
+
};
|
|
75
|
+
let contextOpts = options
|
|
76
|
+
? {
|
|
77
|
+
...options,
|
|
78
|
+
addE2ECoverageEntries: (e) => e2eCoverageEntries.push(e),
|
|
79
|
+
}
|
|
80
|
+
: undefined;
|
|
81
|
+
let { testContext, cleanup } = createTestContext(contextOpts);
|
|
82
|
+
try {
|
|
83
|
+
if (suite.beforeEach) {
|
|
84
|
+
await suite.beforeEach();
|
|
85
|
+
}
|
|
86
|
+
await test.fn(testContext);
|
|
87
|
+
result.status = 'passed';
|
|
88
|
+
results.passed++;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
result.status = 'failed';
|
|
92
|
+
result.error = {
|
|
93
|
+
message: error.message || String(error),
|
|
94
|
+
stack: error.stack,
|
|
95
|
+
};
|
|
96
|
+
results.failed++;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await cleanup();
|
|
100
|
+
if (suite.afterEach) {
|
|
101
|
+
try {
|
|
102
|
+
await suite.afterEach();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('afterEach failed:', error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
result.duration = performance.now() - startTime;
|
|
109
|
+
results.tests.push(result);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (suite.afterAll) {
|
|
113
|
+
try {
|
|
114
|
+
await suite.afterAll();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`afterAll failed in suite "${suite.name}":`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Clear suites in-place so the shared framework module is reset
|
|
122
|
+
// for the next test file (which reuses the same cached module instance)
|
|
123
|
+
suites.length = 0;
|
|
124
|
+
if (e2eCoverageEntries.length > 0) {
|
|
125
|
+
results.e2eBrowserCoverageEntries = e2eCoverageEntries;
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-timers.d.ts","sourceRoot":"","sources":["../../src/lib/fake-timers.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAwD7C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mock } from "./mock.js";
|
|
2
|
+
export function createFakeTimers() {
|
|
3
|
+
let currentTime = 0;
|
|
4
|
+
let nextId = 1;
|
|
5
|
+
let pending = [];
|
|
6
|
+
function schedule(fn, delay, repeatMs) {
|
|
7
|
+
let id = nextId++;
|
|
8
|
+
pending.push({ id, fn, time: currentTime + Math.max(0, delay), repeatMs });
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function cancel(id) {
|
|
12
|
+
pending = pending.filter((t) => t.id !== id);
|
|
13
|
+
}
|
|
14
|
+
let setTimeoutMock = mock.method(globalThis, 'setTimeout', ((fn, delay = 0) => schedule(fn, delay)));
|
|
15
|
+
let clearTimeoutMock = mock.method(globalThis, 'clearTimeout', cancel);
|
|
16
|
+
let setIntervalMock = mock.method(globalThis, 'setInterval', ((fn, delay = 0) => schedule(fn, delay, Math.max(0, delay))));
|
|
17
|
+
let clearIntervalMock = mock.method(globalThis, 'clearInterval', cancel);
|
|
18
|
+
return {
|
|
19
|
+
advance(ms) {
|
|
20
|
+
let targetTime = currentTime + ms;
|
|
21
|
+
while (true) {
|
|
22
|
+
let next = pending.filter((t) => t.time <= targetTime).sort((a, b) => a.time - b.time)[0];
|
|
23
|
+
if (!next)
|
|
24
|
+
break;
|
|
25
|
+
currentTime = next.time;
|
|
26
|
+
pending = pending.filter((t) => t.id !== next.id);
|
|
27
|
+
// Requeue intervals before running the callback so that calling
|
|
28
|
+
// clearInterval(id) from inside the callback can cancel the next firing.
|
|
29
|
+
if (next.repeatMs !== undefined) {
|
|
30
|
+
pending.push({ ...next, time: next.time + Math.max(1, next.repeatMs) });
|
|
31
|
+
}
|
|
32
|
+
next.fn();
|
|
33
|
+
}
|
|
34
|
+
currentTime = targetTime;
|
|
35
|
+
},
|
|
36
|
+
restore() {
|
|
37
|
+
setTimeoutMock.mock.restore?.();
|
|
38
|
+
clearTimeoutMock.mock.restore?.();
|
|
39
|
+
setIntervalMock.mock.restore?.();
|
|
40
|
+
clearIntervalMock.mock.restore?.();
|
|
41
|
+
pending = [];
|
|
42
|
+
currentTime = 0;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { TestContext } from './context.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Groups related tests into a named suite. Suites can be nested snd will be displayed
|
|
4
|
+
* as such or joined with ` > ` in reporter output. Lifecycle hooks registered inside
|
|
5
|
+
* a `describe` block apply only to tests within that block.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* describe('auth', () => {
|
|
9
|
+
* it('logs in', async () => { ... })
|
|
10
|
+
* })
|
|
11
|
+
*
|
|
12
|
+
* // Modifiers
|
|
13
|
+
* describe.skip('skipped suite', () => { ... })
|
|
14
|
+
* describe.only('focused suite', () => { ... })
|
|
15
|
+
* describe.todo('planned suite')
|
|
16
|
+
*
|
|
17
|
+
* @param name - The suite name shown in reporter output.
|
|
18
|
+
* @param fn - A function that registers the tests and lifecycle hooks in this suite.
|
|
19
|
+
*/
|
|
20
|
+
export declare const describe: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
|
|
21
|
+
skip: (name: string, fn: () => void) => void;
|
|
22
|
+
only: (name: string, fn: () => void) => void;
|
|
23
|
+
todo: (name: string) => void;
|
|
24
|
+
};
|
|
25
|
+
type SuiteMeta = {
|
|
26
|
+
skip?: boolean;
|
|
27
|
+
only?: boolean;
|
|
28
|
+
};
|
|
29
|
+
type TestMeta = {
|
|
30
|
+
skip?: boolean;
|
|
31
|
+
only?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type TestFn = (t: TestContext) => void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Defines a single test case. The optional `TestContext` argument `t` provides
|
|
36
|
+
* mock helpers and per-test cleanup registration.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* it('returns 200 for the home route', async () => {
|
|
40
|
+
* const res = await router.fetch('/')
|
|
41
|
+
* assert.equal(res.status, 200)
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // Modifiers
|
|
45
|
+
* it.skip('not ready yet', () => { ... })
|
|
46
|
+
* it.only('focused test', () => { ... })
|
|
47
|
+
* it.todo('coming soon')
|
|
48
|
+
*
|
|
49
|
+
* @param name - The test name shown in reporter output.
|
|
50
|
+
* @param fn - The test body, receiving a {@link TestContext} as its first argument.
|
|
51
|
+
*/
|
|
52
|
+
export declare const it: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
|
|
53
|
+
skip: (name: string, fn?: TestFn | undefined) => void;
|
|
54
|
+
only: (name: string, fn: TestFn) => void;
|
|
55
|
+
todo: (name: string) => void;
|
|
56
|
+
};
|
|
57
|
+
/** Alias for {@link describe}. */
|
|
58
|
+
export declare const suite: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
|
|
59
|
+
skip: (name: string, fn: () => void) => void;
|
|
60
|
+
only: (name: string, fn: () => void) => void;
|
|
61
|
+
todo: (name: string) => void;
|
|
62
|
+
};
|
|
63
|
+
/** Alias for {@link it}. */
|
|
64
|
+
export declare const test: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
|
|
65
|
+
skip: (name: string, fn?: TestFn | undefined) => void;
|
|
66
|
+
only: (name: string, fn: TestFn) => void;
|
|
67
|
+
todo: (name: string) => void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Registers a hook that runs before **each** test in the current suite (or
|
|
71
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
72
|
+
* registration order.
|
|
73
|
+
*
|
|
74
|
+
* @param fn - The setup function to run before each test.
|
|
75
|
+
*/
|
|
76
|
+
export declare function beforeEach(fn: () => void | Promise<void>): void;
|
|
77
|
+
/**
|
|
78
|
+
* Registers a hook that runs after **each** test in the current suite (or
|
|
79
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
80
|
+
* reverse registration order. To run logic after a singular test, use
|
|
81
|
+
* `t.after()` from the {@link TestContext}
|
|
82
|
+
*
|
|
83
|
+
* @param fn - The teardown function to run after each test.
|
|
84
|
+
*/
|
|
85
|
+
export declare function afterEach(fn: () => void | Promise<void>): void;
|
|
86
|
+
/**
|
|
87
|
+
* Registers a hook that runs once before **all** tests in the current suite
|
|
88
|
+
* (or globally if called outside a `describe`). Multiple calls are chained in
|
|
89
|
+
* registration order.
|
|
90
|
+
*
|
|
91
|
+
* @param fn - The setup function to run once before all tests in the suite.
|
|
92
|
+
*/
|
|
93
|
+
export declare function beforeAll(fn: () => void | Promise<void>): void;
|
|
94
|
+
/**
|
|
95
|
+
* Registers a hook that runs once after **all** tests in the current suite (or
|
|
96
|
+
* globally if called outside a `describe`). Multiple calls are chained in
|
|
97
|
+
* reverse registration order.
|
|
98
|
+
*
|
|
99
|
+
* @param fn - The teardown function to run once after all tests in the suite.
|
|
100
|
+
*/
|
|
101
|
+
export declare function afterAll(fn: () => void | Promise<void>): void;
|
|
102
|
+
/** Alias for {@link beforeAll} — matches the `node:test` API. */
|
|
103
|
+
export declare const before: typeof beforeAll;
|
|
104
|
+
/** Alias for {@link afterAll} — matches the `node:test` API. */
|
|
105
|
+
export declare const after: typeof afterAll;
|
|
106
|
+
export {};
|
|
107
|
+
//# sourceMappingURL=framework.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework.d.ts","sourceRoot":"","sources":["../../src/lib/framework.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAkF/C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ;;;;CAiBpB,CAAA;AAED,KAAK,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACnD,KAAK,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAClD,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAUtD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,EAAE;;;;CAiBd,CAAA;AAED,kCAAkC;AAClC,eAAO,MAAM,KAAK;;;;CAAW,CAAA;AAC7B,4BAA4B;AAC5B,eAAO,MAAM,IAAI;;;;CAAK,CAAA;AA2BtB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGxD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGtD;AAED,mEAAiE;AACjE,eAAO,MAAM,MAAM,kBAAY,CAAA;AAC/B,kEAAgE;AAChE,eAAO,MAAM,KAAK,iBAAW,CAAA"}
|