@nexustools/nwjstestrunner 0.0.0-stage → 1.0.0-beta.1
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/AGENTS.md +21 -0
- package/README.md +142 -2
- package/bin/nwjstestrunner.js +13 -0
- package/dist/cli/nwjs-cli-launcher.d.ts +10 -0
- package/dist/cli/nwjs-cli-launcher.d.ts.map +1 -0
- package/dist/cli/nwjs-cli-launcher.js +281 -0
- package/dist/cli/nwjs-cli-launcher.js.map +1 -0
- package/dist/config/harness-config-loader.d.ts +10 -0
- package/dist/config/harness-config-loader.d.ts.map +1 -0
- package/dist/config/harness-config-loader.js +242 -0
- package/dist/config/harness-config-loader.js.map +1 -0
- package/dist/core/test-runner.d.ts +61 -0
- package/dist/core/test-runner.d.ts.map +1 -0
- package/dist/core/test-runner.js +450 -0
- package/dist/core/test-runner.js.map +1 -0
- package/dist/esm/index.d.mts +2 -0
- package/dist/esm/index.mjs +26 -0
- package/dist/harness/graphical-overlay-view.d.ts +18 -0
- package/dist/harness/graphical-overlay-view.d.ts.map +1 -0
- package/dist/harness/graphical-overlay-view.js +213 -0
- package/dist/harness/graphical-overlay-view.js.map +1 -0
- package/dist/harness/nwjs-graphical-test-harness.d.ts +40 -0
- package/dist/harness/nwjs-graphical-test-harness.d.ts.map +1 -0
- package/dist/harness/nwjs-graphical-test-harness.js +398 -0
- package/dist/harness/nwjs-graphical-test-harness.js.map +1 -0
- package/dist/harness/test-http-server.d.ts +12 -0
- package/dist/harness/test-http-server.d.ts.map +1 -0
- package/dist/harness/test-http-server.js +183 -0
- package/dist/harness/test-http-server.js.map +1 -0
- package/dist/harness/test-log-writer.d.ts +29 -0
- package/dist/harness/test-log-writer.d.ts.map +1 -0
- package/dist/harness/test-log-writer.js +173 -0
- package/dist/harness/test-log-writer.js.map +1 -0
- package/dist/harness/webview-test-controller.d.ts +18 -0
- package/dist/harness/webview-test-controller.d.ts.map +1 -0
- package/dist/harness/webview-test-controller.js +163 -0
- package/dist/harness/webview-test-controller.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/matchers/deep-equality-helper.d.ts +10 -0
- package/dist/matchers/deep-equality-helper.d.ts.map +1 -0
- package/dist/matchers/deep-equality-helper.js +188 -0
- package/dist/matchers/deep-equality-helper.js.map +1 -0
- package/dist/matchers/expect-matcher.d.ts +7 -0
- package/dist/matchers/expect-matcher.d.ts.map +1 -0
- package/dist/matchers/expect-matcher.js +261 -0
- package/dist/matchers/expect-matcher.js.map +1 -0
- package/dist/mock/jest-mock-factory.d.ts +20 -0
- package/dist/mock/jest-mock-factory.d.ts.map +1 -0
- package/dist/mock/jest-mock-factory.js +141 -0
- package/dist/mock/jest-mock-factory.js.map +1 -0
- package/dist/tests/NwjsTestRunnerSuites.d.ts +3 -0
- package/dist/tests/NwjsTestRunnerSuites.d.ts.map +1 -0
- package/dist/tests/NwjsTestRunnerSuites.js +266 -0
- package/dist/tests/NwjsTestRunnerSuites.js.map +1 -0
- package/dist/types/test-runner-types.d.ts +321 -0
- package/dist/types/test-runner-types.d.ts.map +1 -0
- package/dist/types/test-runner-types.js +3 -0
- package/dist/types/test-runner-types.js.map +1 -0
- package/package.json +56 -4
- package/runner/demo-client.html +70 -0
- package/runner/index.html +223 -0
- package/runner/package.json +27 -0
- package/src/cli/nwjs-cli-launcher.ts +243 -0
- package/src/config/harness-config-loader.ts +214 -0
- package/src/core/test-runner.ts +553 -0
- package/src/harness/graphical-overlay-view.ts +219 -0
- package/src/harness/nwjs-graphical-test-harness.ts +468 -0
- package/src/harness/test-http-server.ts +162 -0
- package/src/harness/test-log-writer.ts +154 -0
- package/src/harness/webview-test-controller.ts +192 -0
- package/src/index.ts +28 -0
- package/src/matchers/deep-equality-helper.ts +193 -0
- package/src/matchers/expect-matcher.ts +361 -0
- package/src/mock/jest-mock-factory.ts +170 -0
- package/src/tests/NwjsTestRunnerSuites.ts +267 -0
- package/src/types/test-runner-types.ts +335 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { expect } from '../matchers/expect-matcher';
|
|
2
|
+
import { jest } from '../mock/jest-mock-factory';
|
|
3
|
+
import {
|
|
4
|
+
LifecycleHookFn,
|
|
5
|
+
TestCase,
|
|
6
|
+
TestCaseFn,
|
|
7
|
+
TestCaseResult,
|
|
8
|
+
TestFailure,
|
|
9
|
+
TestProgressEvent,
|
|
10
|
+
TestRunOptions,
|
|
11
|
+
TestSuite,
|
|
12
|
+
TestSuiteResult,
|
|
13
|
+
TestSummary
|
|
14
|
+
} from '../types/test-runner-types';
|
|
15
|
+
|
|
16
|
+
export interface SuiteBuilderScope {
|
|
17
|
+
it: (testName: string, fn: TestCaseFn, timeoutMs?: number) => void;
|
|
18
|
+
test: (testName: string, fn: TestCaseFn, timeoutMs?: number) => void;
|
|
19
|
+
beforeAll: (fn: LifecycleHookFn) => void;
|
|
20
|
+
afterAll: (fn: LifecycleHookFn) => void;
|
|
21
|
+
beforeEach: (fn: LifecycleHookFn) => void;
|
|
22
|
+
afterEach: (fn: LifecycleHookFn) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TestRunner {
|
|
26
|
+
private static instance: TestRunner | null = null;
|
|
27
|
+
private suites: TestSuite[] = [];
|
|
28
|
+
private suiteStack: string[] = [];
|
|
29
|
+
private currentSuiteName = 'DefaultSuite';
|
|
30
|
+
|
|
31
|
+
public get suiteCount(): number {
|
|
32
|
+
return this.suites.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public get totalCasesCount(): number {
|
|
36
|
+
return this.suites.reduce((sum, s) => sum + s.cases.length, 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getSuites(): ReadonlyArray<TestSuite> {
|
|
40
|
+
return this.suites;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static getInstance(): TestRunner {
|
|
44
|
+
if (!TestRunner.instance) {
|
|
45
|
+
TestRunner.instance = new TestRunner();
|
|
46
|
+
}
|
|
47
|
+
return TestRunner.instance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public reset(): void {
|
|
51
|
+
this.suites = [];
|
|
52
|
+
this.suiteStack = [];
|
|
53
|
+
this.currentSuiteName = 'DefaultSuite';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getOrCreateSuite(name: string, options?: { skip?: boolean; only?: boolean }): TestSuite {
|
|
57
|
+
let suite = this.suites.find((s) => s.name === name);
|
|
58
|
+
if (!suite) {
|
|
59
|
+
suite = {
|
|
60
|
+
name,
|
|
61
|
+
cases: [],
|
|
62
|
+
beforeAllHooks: [],
|
|
63
|
+
afterAllHooks: [],
|
|
64
|
+
beforeEachHooks: [],
|
|
65
|
+
afterEachHooks: [],
|
|
66
|
+
skip: options?.skip,
|
|
67
|
+
only: options?.only
|
|
68
|
+
};
|
|
69
|
+
this.suites.push(suite);
|
|
70
|
+
} else {
|
|
71
|
+
if (options?.skip) suite.skip = true;
|
|
72
|
+
if (options?.only) suite.only = true;
|
|
73
|
+
}
|
|
74
|
+
return suite;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public registerSuite(
|
|
78
|
+
name: string,
|
|
79
|
+
defineFn: (suite: SuiteBuilderScope) => void,
|
|
80
|
+
options?: { skip?: boolean; only?: boolean }
|
|
81
|
+
): void {
|
|
82
|
+
const fullName =
|
|
83
|
+
this.suiteStack.length > 0 ? `${this.suiteStack.join(' > ')} > ${name}` : name;
|
|
84
|
+
const previousSuiteName = this.currentSuiteName;
|
|
85
|
+
this.suiteStack.push(name);
|
|
86
|
+
this.currentSuiteName = fullName;
|
|
87
|
+
|
|
88
|
+
const currentSuite = this.getOrCreateSuite(fullName, options);
|
|
89
|
+
|
|
90
|
+
const testScope: SuiteBuilderScope = {
|
|
91
|
+
it: (testName: string, fn: TestCaseFn, timeoutMs?: number) => {
|
|
92
|
+
currentSuite.cases.push({ name: testName, fn, timeoutMs });
|
|
93
|
+
},
|
|
94
|
+
test: (testName: string, fn: TestCaseFn, timeoutMs?: number) => {
|
|
95
|
+
currentSuite.cases.push({ name: testName, fn, timeoutMs });
|
|
96
|
+
},
|
|
97
|
+
beforeAll: (fn: LifecycleHookFn) => {
|
|
98
|
+
currentSuite.beforeAllHooks.push(fn);
|
|
99
|
+
},
|
|
100
|
+
afterAll: (fn: LifecycleHookFn) => {
|
|
101
|
+
currentSuite.afterAllHooks.push(fn);
|
|
102
|
+
},
|
|
103
|
+
beforeEach: (fn: LifecycleHookFn) => {
|
|
104
|
+
currentSuite.beforeEachHooks.push(fn);
|
|
105
|
+
},
|
|
106
|
+
afterEach: (fn: LifecycleHookFn) => {
|
|
107
|
+
currentSuite.afterEachHooks.push(fn);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
defineFn(testScope);
|
|
113
|
+
} finally {
|
|
114
|
+
this.suiteStack.pop();
|
|
115
|
+
this.currentSuiteName =
|
|
116
|
+
this.suiteStack.length > 0 ? this.suiteStack.join(' > ') : previousSuiteName;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public addCase(
|
|
121
|
+
suiteName: string,
|
|
122
|
+
testName: string,
|
|
123
|
+
fn: TestCaseFn,
|
|
124
|
+
options?: { timeoutMs?: number; skip?: boolean; only?: boolean }
|
|
125
|
+
): void {
|
|
126
|
+
const suite = this.getOrCreateSuite(suiteName);
|
|
127
|
+
suite.cases.push({
|
|
128
|
+
name: testName,
|
|
129
|
+
fn,
|
|
130
|
+
timeoutMs: options?.timeoutMs,
|
|
131
|
+
skip: options?.skip,
|
|
132
|
+
only: options?.only
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public addBeforeAll(fn: LifecycleHookFn, suiteName = this.currentSuiteName): void {
|
|
137
|
+
this.getOrCreateSuite(suiteName).beforeAllHooks.push(fn);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public addAfterAll(fn: LifecycleHookFn, suiteName = this.currentSuiteName): void {
|
|
141
|
+
this.getOrCreateSuite(suiteName).afterAllHooks.push(fn);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public addBeforeEach(fn: LifecycleHookFn, suiteName = this.currentSuiteName): void {
|
|
145
|
+
this.getOrCreateSuite(suiteName).beforeEachHooks.push(fn);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public addAfterEach(fn: LifecycleHookFn, suiteName = this.currentSuiteName): void {
|
|
149
|
+
this.getOrCreateSuite(suiteName).afterEachHooks.push(fn);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public getCurrentSuiteName(): string {
|
|
153
|
+
return this.currentSuiteName;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async executeWithTimeout(fn: () => Promise<void> | void, timeoutMs: number, label: string): Promise<void> {
|
|
157
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
158
|
+
try {
|
|
159
|
+
const runPromise = Promise.resolve().then(() => fn());
|
|
160
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
161
|
+
timer = setTimeout(
|
|
162
|
+
() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
|
|
163
|
+
timeoutMs
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
await Promise.race([runPromise, timeoutPromise]);
|
|
167
|
+
} finally {
|
|
168
|
+
if (timer !== undefined) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async runAll(optionsOrMaxDurationMs: number | TestRunOptions = 5000): Promise<TestSummary> {
|
|
175
|
+
const options: TestRunOptions =
|
|
176
|
+
typeof optionsOrMaxDurationMs === 'number'
|
|
177
|
+
? { maxDurationMs: optionsOrMaxDurationMs }
|
|
178
|
+
: optionsOrMaxDurationMs;
|
|
179
|
+
|
|
180
|
+
const maxDurationMs = options.maxDurationMs ?? 15000;
|
|
181
|
+
const defaultTestTimeoutMs = options.defaultTestTimeoutMs ?? 2000;
|
|
182
|
+
const filter = options.filter;
|
|
183
|
+
const onProgress = options.onProgress;
|
|
184
|
+
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
let total = 0;
|
|
187
|
+
let passed = 0;
|
|
188
|
+
let failed = 0;
|
|
189
|
+
let skipped = 0;
|
|
190
|
+
const failures: TestFailure[] = [];
|
|
191
|
+
const suiteResults: TestSuiteResult[] = [];
|
|
192
|
+
const logs: string[] = [];
|
|
193
|
+
|
|
194
|
+
const hasOnlySuite = this.suites.some((s) => s.only);
|
|
195
|
+
const hasOnlyCase = this.suites.some((s) => s.cases.some((c) => c.only));
|
|
196
|
+
const totalExpected = this.totalCasesCount;
|
|
197
|
+
|
|
198
|
+
logs.push('=== LIVE JEST CLIENT TEST SUITE RESULTS ===');
|
|
199
|
+
logs.push(`Timestamp: ${new Date().toISOString()}`);
|
|
200
|
+
logs.push('');
|
|
201
|
+
|
|
202
|
+
const matchesFilter = (fullName: string): boolean => {
|
|
203
|
+
if (!filter) return true;
|
|
204
|
+
if (typeof filter === 'string') {
|
|
205
|
+
return fullName.toLowerCase().includes(filter.toLowerCase());
|
|
206
|
+
}
|
|
207
|
+
return filter.test(fullName);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const emitProgress = (partial: Omit<TestProgressEvent, 'completedCount' | 'totalCount' | 'passedCount' | 'failedCount' | 'skippedCount'>): void => {
|
|
211
|
+
if (!onProgress) return;
|
|
212
|
+
try {
|
|
213
|
+
onProgress({
|
|
214
|
+
...partial,
|
|
215
|
+
completedCount: passed + failed + skipped,
|
|
216
|
+
totalCount: totalExpected,
|
|
217
|
+
passedCount: passed,
|
|
218
|
+
failedCount: failed,
|
|
219
|
+
skippedCount: skipped
|
|
220
|
+
});
|
|
221
|
+
} catch (_e) {}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const suite of this.suites) {
|
|
225
|
+
if (Date.now() - startTime > maxDurationMs) {
|
|
226
|
+
logs.push(
|
|
227
|
+
`[TIMEOUT] Hard deadline limit (${maxDurationMs}ms) reached. Aborting remaining test suites.`
|
|
228
|
+
);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const suiteStart = Date.now();
|
|
233
|
+
const suiteCasesResults: TestCaseResult[] = [];
|
|
234
|
+
let suitePassed = 0;
|
|
235
|
+
let suiteFailed = 0;
|
|
236
|
+
let suiteSkipped = 0;
|
|
237
|
+
|
|
238
|
+
logs.push(`--- SUITE: ${suite.name} ---`);
|
|
239
|
+
emitProgress({ type: 'suite_start', suiteName: suite.name });
|
|
240
|
+
|
|
241
|
+
const shouldSkipWholeSuite = Boolean(suite.skip || (hasOnlySuite && !suite.only));
|
|
242
|
+
|
|
243
|
+
let beforeAllError: Error | null = null;
|
|
244
|
+
if (!shouldSkipWholeSuite && suite.beforeAllHooks.length > 0) {
|
|
245
|
+
for (const hook of suite.beforeAllHooks) {
|
|
246
|
+
try {
|
|
247
|
+
await this.executeWithTimeout(hook, defaultTestTimeoutMs, 'beforeAll hook');
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
beforeAllError = err instanceof Error ? err : new Error(String(err));
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const testCase of suite.cases) {
|
|
256
|
+
if (Date.now() - startTime > maxDurationMs) {
|
|
257
|
+
logs.push(
|
|
258
|
+
`[TIMEOUT] Hard deadline limit (${maxDurationMs}ms) reached. Aborting remaining tests.`
|
|
259
|
+
);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const fullName = `${suite.name} > ${testCase.name}`;
|
|
264
|
+
const shouldSkipCase =
|
|
265
|
+
shouldSkipWholeSuite ||
|
|
266
|
+
Boolean(testCase.skip) ||
|
|
267
|
+
(hasOnlyCase && !suite.only && !testCase.only) ||
|
|
268
|
+
!matchesFilter(fullName);
|
|
269
|
+
|
|
270
|
+
if (shouldSkipCase) {
|
|
271
|
+
skipped++;
|
|
272
|
+
suiteSkipped++;
|
|
273
|
+
suiteCasesResults.push({
|
|
274
|
+
suiteName: suite.name,
|
|
275
|
+
testName: testCase.name,
|
|
276
|
+
status: 'skipped',
|
|
277
|
+
durationMs: 0
|
|
278
|
+
});
|
|
279
|
+
logs.push(`[SKIPPED] ${fullName}`);
|
|
280
|
+
emitProgress({
|
|
281
|
+
type: 'test_complete',
|
|
282
|
+
suiteName: suite.name,
|
|
283
|
+
testName: testCase.name,
|
|
284
|
+
status: 'skipped'
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
total++;
|
|
290
|
+
const caseStart = Date.now();
|
|
291
|
+
const caseTimeout = testCase.timeoutMs ?? defaultTestTimeoutMs;
|
|
292
|
+
|
|
293
|
+
emitProgress({
|
|
294
|
+
type: 'test_start',
|
|
295
|
+
suiteName: suite.name,
|
|
296
|
+
testName: testCase.name
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
if (beforeAllError) {
|
|
301
|
+
throw beforeAllError;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const beforeHook of suite.beforeEachHooks) {
|
|
305
|
+
await this.executeWithTimeout(beforeHook, caseTimeout, 'beforeEach hook');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let testExecError: any = null;
|
|
309
|
+
try {
|
|
310
|
+
await this.executeWithTimeout(testCase.fn, caseTimeout, 'Test');
|
|
311
|
+
} catch (err) {
|
|
312
|
+
testExecError = err;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let afterEachError: any = null;
|
|
316
|
+
for (const afterHook of suite.afterEachHooks) {
|
|
317
|
+
try {
|
|
318
|
+
await this.executeWithTimeout(afterHook, caseTimeout, 'afterEach hook');
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (!afterEachError) afterEachError = err;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (testExecError) {
|
|
325
|
+
throw testExecError;
|
|
326
|
+
}
|
|
327
|
+
if (afterEachError) {
|
|
328
|
+
throw afterEachError;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const caseDuration = Date.now() - caseStart;
|
|
332
|
+
passed++;
|
|
333
|
+
suitePassed++;
|
|
334
|
+
suiteCasesResults.push({
|
|
335
|
+
suiteName: suite.name,
|
|
336
|
+
testName: testCase.name,
|
|
337
|
+
status: 'passed',
|
|
338
|
+
durationMs: caseDuration
|
|
339
|
+
});
|
|
340
|
+
logs.push(`[PASSED] ${fullName}`);
|
|
341
|
+
emitProgress({
|
|
342
|
+
type: 'test_complete',
|
|
343
|
+
suiteName: suite.name,
|
|
344
|
+
testName: testCase.name,
|
|
345
|
+
status: 'passed'
|
|
346
|
+
});
|
|
347
|
+
} catch (err: any) {
|
|
348
|
+
const caseDuration = Date.now() - caseStart;
|
|
349
|
+
failed++;
|
|
350
|
+
suiteFailed++;
|
|
351
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
352
|
+
failures.push({ suiteName: suite.name, testName: testCase.name, error: errorObj });
|
|
353
|
+
suiteCasesResults.push({
|
|
354
|
+
suiteName: suite.name,
|
|
355
|
+
testName: testCase.name,
|
|
356
|
+
status: 'failed',
|
|
357
|
+
durationMs: caseDuration,
|
|
358
|
+
error: errorObj
|
|
359
|
+
});
|
|
360
|
+
logs.push(`[FAILED] ${fullName}`);
|
|
361
|
+
logs.push(` Error: ${errorObj.message}`);
|
|
362
|
+
if (errorObj.stack) {
|
|
363
|
+
logs.push(
|
|
364
|
+
` Stack Trace:\n${errorObj.stack
|
|
365
|
+
.split('\n')
|
|
366
|
+
.map((l) => ' ' + l)
|
|
367
|
+
.join('\n')}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
logs.push('');
|
|
371
|
+
emitProgress({
|
|
372
|
+
type: 'test_complete',
|
|
373
|
+
suiteName: suite.name,
|
|
374
|
+
testName: testCase.name,
|
|
375
|
+
status: 'failed',
|
|
376
|
+
error: errorObj
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!shouldSkipWholeSuite && suite.afterAllHooks.length > 0) {
|
|
382
|
+
for (const hook of suite.afterAllHooks) {
|
|
383
|
+
try {
|
|
384
|
+
await this.executeWithTimeout(hook, defaultTestTimeoutMs, 'afterAll hook');
|
|
385
|
+
} catch (err: any) {
|
|
386
|
+
failed++;
|
|
387
|
+
suiteFailed++;
|
|
388
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
389
|
+
failures.push({
|
|
390
|
+
suiteName: suite.name,
|
|
391
|
+
testName: 'afterAll',
|
|
392
|
+
error: errorObj
|
|
393
|
+
});
|
|
394
|
+
logs.push(`[FAILED] ${suite.name} > afterAll`);
|
|
395
|
+
logs.push(` Error: ${errorObj.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
suiteResults.push({
|
|
401
|
+
suiteName: suite.name,
|
|
402
|
+
passed: suitePassed,
|
|
403
|
+
failed: suiteFailed,
|
|
404
|
+
skipped: suiteSkipped,
|
|
405
|
+
durationMs: Date.now() - suiteStart,
|
|
406
|
+
cases: suiteCasesResults
|
|
407
|
+
});
|
|
408
|
+
logs.push('');
|
|
409
|
+
emitProgress({ type: 'suite_complete', suiteName: suite.name });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const durationMs = Date.now() - startTime;
|
|
413
|
+
logs.unshift(
|
|
414
|
+
`SUMMARY: Total: ${total} | Passed: ${passed} | Failed: ${failed}${skipped > 0 ? ` | Skipped: ${skipped}` : ''}\n`
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
total,
|
|
419
|
+
passed,
|
|
420
|
+
failed,
|
|
421
|
+
skipped,
|
|
422
|
+
durationMs,
|
|
423
|
+
failures,
|
|
424
|
+
suiteResults,
|
|
425
|
+
logOutput: logs.join('\n')
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public static assertEquals<T>(actual: T, expected: T, message?: string): void {
|
|
430
|
+
if (actual !== expected) {
|
|
431
|
+
throw new Error(message || `Assertion failed: expected '${expected}', got '${actual}'`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public static assertTrue(value: boolean, message?: string): void {
|
|
436
|
+
if (value !== true) {
|
|
437
|
+
throw new Error(message || `Assertion failed: expected true, got ${value}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
public static assertFalse(value: boolean, message?: string): void {
|
|
442
|
+
if (value !== false) {
|
|
443
|
+
throw new Error(message || `Assertion failed: expected false, got ${value}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public static assertNotNull<T>(value: T | null | undefined, message?: string): T {
|
|
448
|
+
if (value === null || value === undefined) {
|
|
449
|
+
throw new Error(message || 'Assertion failed: value is null or undefined');
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export interface DescribeFunction {
|
|
456
|
+
(suiteName: string, fn: () => void): void;
|
|
457
|
+
skip: (suiteName: string, fn: () => void) => void;
|
|
458
|
+
only: (suiteName: string, fn: () => void) => void;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface ItFunction {
|
|
462
|
+
(testName: string, fn: TestCaseFn, timeoutMs?: number): void;
|
|
463
|
+
skip: (testName: string, fn?: TestCaseFn, timeoutMs?: number) => void;
|
|
464
|
+
only: (testName: string, fn: TestCaseFn, timeoutMs?: number) => void;
|
|
465
|
+
todo: (testName: string) => void;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const describeBase = function (suiteName: string, fn: () => void): void {
|
|
469
|
+
const runner = TestRunner.getInstance();
|
|
470
|
+
runner.registerSuite(suiteName, () => {
|
|
471
|
+
fn();
|
|
472
|
+
});
|
|
473
|
+
} as DescribeFunction;
|
|
474
|
+
|
|
475
|
+
describeBase.skip = (suiteName: string, fn: () => void): void => {
|
|
476
|
+
const runner = TestRunner.getInstance();
|
|
477
|
+
runner.registerSuite(
|
|
478
|
+
suiteName,
|
|
479
|
+
() => {
|
|
480
|
+
fn();
|
|
481
|
+
},
|
|
482
|
+
{ skip: true }
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
describeBase.only = (suiteName: string, fn: () => void): void => {
|
|
487
|
+
const runner = TestRunner.getInstance();
|
|
488
|
+
runner.registerSuite(
|
|
489
|
+
suiteName,
|
|
490
|
+
() => {
|
|
491
|
+
fn();
|
|
492
|
+
},
|
|
493
|
+
{ only: true }
|
|
494
|
+
);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export const describe: DescribeFunction = describeBase;
|
|
498
|
+
|
|
499
|
+
const itBase = function (testName: string, fn: TestCaseFn, timeoutMs?: number): void {
|
|
500
|
+
const runner = TestRunner.getInstance();
|
|
501
|
+
const suiteName = runner.getCurrentSuiteName();
|
|
502
|
+
runner.addCase(suiteName, testName, fn, { timeoutMs });
|
|
503
|
+
} as ItFunction;
|
|
504
|
+
|
|
505
|
+
itBase.skip = (testName: string, fn?: TestCaseFn, timeoutMs?: number): void => {
|
|
506
|
+
const runner = TestRunner.getInstance();
|
|
507
|
+
const suiteName = runner.getCurrentSuiteName();
|
|
508
|
+
runner.addCase(suiteName, testName, fn || (() => {}), { timeoutMs, skip: true });
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
itBase.only = (testName: string, fn: TestCaseFn, timeoutMs?: number): void => {
|
|
512
|
+
const runner = TestRunner.getInstance();
|
|
513
|
+
const suiteName = runner.getCurrentSuiteName();
|
|
514
|
+
runner.addCase(suiteName, testName, fn, { timeoutMs, only: true });
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
itBase.todo = (testName: string): void => {
|
|
518
|
+
const runner = TestRunner.getInstance();
|
|
519
|
+
const suiteName = runner.getCurrentSuiteName();
|
|
520
|
+
runner.addCase(suiteName, testName, () => {}, { skip: true });
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
export const it: ItFunction = itBase;
|
|
524
|
+
export const test: ItFunction = itBase;
|
|
525
|
+
|
|
526
|
+
export function beforeAll(fn: LifecycleHookFn): void {
|
|
527
|
+
TestRunner.getInstance().addBeforeAll(fn);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function afterAll(fn: LifecycleHookFn): void {
|
|
531
|
+
TestRunner.getInstance().addAfterAll(fn);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function beforeEach(fn: LifecycleHookFn): void {
|
|
535
|
+
TestRunner.getInstance().addBeforeEach(fn);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function afterEach(fn: LifecycleHookFn): void {
|
|
539
|
+
TestRunner.getInstance().addAfterEach(fn);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function installJestGlobals(target: any = globalThis): void {
|
|
543
|
+
if (!target) return;
|
|
544
|
+
target.describe = describe;
|
|
545
|
+
target.it = it;
|
|
546
|
+
target.test = test;
|
|
547
|
+
target.expect = expect;
|
|
548
|
+
target.beforeAll = beforeAll;
|
|
549
|
+
target.afterAll = afterAll;
|
|
550
|
+
target.beforeEach = beforeEach;
|
|
551
|
+
target.afterEach = afterEach;
|
|
552
|
+
target.jest = jest;
|
|
553
|
+
}
|