@gjsify/unit 0.0.3 → 0.1.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 +45 -68
- package/lib/esm/index.js +293 -34
- package/lib/types/index.d.ts +55 -10
- package/package.json +16 -20
- package/src/index.spec.ts +47 -1
- package/src/index.ts +329 -33
- package/tsconfig.json +25 -7
- package/lib/cjs/index.js +0 -360
- package/lib/cjs/spy.js +0 -139
- package/test.gjs.mjs +0 -35632
- package/test.node.mjs +0 -1219
- package/tsconfig.types.json +0 -8
package/src/index.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import "@girs/gjs";
|
|
4
4
|
|
|
5
|
-
import type GLib from '
|
|
5
|
+
import type GLib from '@girs/glib-2.0';
|
|
6
6
|
export * from './spy.js';
|
|
7
|
-
import nodeAssert from 'assert';
|
|
7
|
+
import nodeAssert from 'node:assert';
|
|
8
|
+
import { quitMainLoop } from '@gjsify/utils/main-loop';
|
|
8
9
|
|
|
9
10
|
const mainloop: GLib.MainLoop | undefined = (globalThis as any)?.imports?.mainloop;
|
|
10
11
|
|
|
@@ -12,6 +13,70 @@ let countTestsOverall = 0;
|
|
|
12
13
|
let countTestsFailed = 0;
|
|
13
14
|
let countTestsIgnored = 0;
|
|
14
15
|
let runtime = '';
|
|
16
|
+
let runStartTime = 0;
|
|
17
|
+
|
|
18
|
+
export interface TimeoutConfig {
|
|
19
|
+
/** Per-it() timeout in ms. Default: 5000. 0 = disabled. */
|
|
20
|
+
testTimeout: number;
|
|
21
|
+
/** Per-describe() timeout in ms. Default: 30000. 0 = disabled. */
|
|
22
|
+
suiteTimeout: number;
|
|
23
|
+
/** Global run timeout in ms. Default: 120000. 0 = disabled. */
|
|
24
|
+
runTimeout: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_TIMEOUT_CONFIG: TimeoutConfig = {
|
|
28
|
+
testTimeout: 5000,
|
|
29
|
+
suiteTimeout: 30000,
|
|
30
|
+
runTimeout: 120000,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let timeoutConfig: TimeoutConfig = { ...DEFAULT_TIMEOUT_CONFIG };
|
|
34
|
+
|
|
35
|
+
class TimeoutError extends Error {
|
|
36
|
+
constructor(label: string, timeoutMs: number) {
|
|
37
|
+
super(`Timeout: "${label}" exceeded ${timeoutMs}ms`);
|
|
38
|
+
this.name = 'TimeoutError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function withTimeout<T>(
|
|
43
|
+
fn: () => T | Promise<T>,
|
|
44
|
+
timeoutMs: number,
|
|
45
|
+
label: string
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
if (timeoutMs <= 0) return fn();
|
|
48
|
+
|
|
49
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
50
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
51
|
+
timeoutId = setTimeout(() => reject(new TimeoutError(label, timeoutMs)), timeoutMs);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const fnPromise = Promise.resolve(fn());
|
|
55
|
+
fnPromise.catch(() => {}); // Prevent unhandled rejection if it fails after timeout
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await Promise.race([fnPromise, timeoutPromise]);
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timeoutId!);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const configure = (overrides: Partial<TimeoutConfig>) => {
|
|
65
|
+
timeoutConfig = { ...timeoutConfig, ...overrides };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function applyEnvOverrides() {
|
|
69
|
+
try {
|
|
70
|
+
const env = (globalThis as any).process?.env;
|
|
71
|
+
if (!env) return;
|
|
72
|
+
const t = parseInt(env.GJSIFY_TEST_TIMEOUT, 10);
|
|
73
|
+
if (!isNaN(t) && t >= 0) timeoutConfig.testTimeout = t;
|
|
74
|
+
const s = parseInt(env.GJSIFY_SUITE_TIMEOUT, 10);
|
|
75
|
+
if (!isNaN(s) && s >= 0) timeoutConfig.suiteTimeout = s;
|
|
76
|
+
const r = parseInt(env.GJSIFY_RUN_TIMEOUT, 10);
|
|
77
|
+
if (!isNaN(r) && r >= 0) timeoutConfig.runTimeout = r;
|
|
78
|
+
} catch (_e) { /* process.env may not be available */ }
|
|
79
|
+
}
|
|
15
80
|
|
|
16
81
|
const RED = '\x1B[31m';
|
|
17
82
|
const GREEN = '\x1B[32m';
|
|
@@ -19,13 +84,22 @@ const BLUE = '\x1b[34m';
|
|
|
19
84
|
const GRAY = '\x1B[90m';
|
|
20
85
|
const RESET = '\x1B[39m';
|
|
21
86
|
|
|
87
|
+
const now = (): number =>
|
|
88
|
+
(globalThis as any).performance?.now?.() ?? Date.now();
|
|
89
|
+
|
|
90
|
+
const formatDuration = (ms: number): string => {
|
|
91
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
92
|
+
if (ms >= 100) return `${Math.round(ms)}ms`;
|
|
93
|
+
return `${ms.toFixed(1)}ms`;
|
|
94
|
+
};
|
|
95
|
+
|
|
22
96
|
export interface Namespaces {
|
|
23
97
|
[key: string]: () => (Promise<void>) | Namespaces;
|
|
24
98
|
}
|
|
25
99
|
|
|
26
100
|
export type Callback = () => Promise<void>;
|
|
27
101
|
|
|
28
|
-
export type Runtime = 'Gjs' | 'Deno' | 'Node.js' | 'Unknown' | 'Browser';
|
|
102
|
+
export type Runtime = 'Gjs' | 'Deno' | 'Node.js' | 'Unknown' | 'Browser' | 'Display';
|
|
29
103
|
|
|
30
104
|
// Makes this work on Gjs and Node.js
|
|
31
105
|
export const print = globalThis.print || console.log;
|
|
@@ -45,8 +119,10 @@ class MatcherFactory {
|
|
|
45
119
|
triggerResult(success: boolean, msg: string) {
|
|
46
120
|
if( (success && !this.positive) ||
|
|
47
121
|
(!success && this.positive) ) {
|
|
122
|
+
const error = new Error(msg);
|
|
123
|
+
(error as any).__testFailureCounted = true;
|
|
48
124
|
++countTestsFailed;
|
|
49
|
-
throw
|
|
125
|
+
throw error;
|
|
50
126
|
}
|
|
51
127
|
}
|
|
52
128
|
|
|
@@ -72,6 +148,23 @@ class MatcherFactory {
|
|
|
72
148
|
);
|
|
73
149
|
}
|
|
74
150
|
|
|
151
|
+
toStrictEqual(expectedValue: any) {
|
|
152
|
+
let success = true;
|
|
153
|
+
let errorMessage = '';
|
|
154
|
+
try {
|
|
155
|
+
nodeAssert.deepStrictEqual(this.actualValue, expectedValue);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
success = false;
|
|
158
|
+
errorMessage = e.message || '';
|
|
159
|
+
}
|
|
160
|
+
this.triggerResult(success,
|
|
161
|
+
` Expected values to be deeply strictly equal\n` +
|
|
162
|
+
` Expected: ${JSON.stringify(expectedValue)}\n` +
|
|
163
|
+
` Actual: ${JSON.stringify(this.actualValue)}` +
|
|
164
|
+
(errorMessage ? `\n ${errorMessage}` : '')
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
75
168
|
toEqualArray(expectedValue: Array<any> | Uint8Array) {
|
|
76
169
|
|
|
77
170
|
let success = Array.isArray(this.actualValue) && Array.isArray(expectedValue) && this.actualValue.length === expectedValue.length;
|
|
@@ -90,6 +183,21 @@ class MatcherFactory {
|
|
|
90
183
|
);
|
|
91
184
|
}
|
|
92
185
|
|
|
186
|
+
toBeInstanceOf(expectedType: Function) {
|
|
187
|
+
this.triggerResult(this.actualValue instanceof expectedType,
|
|
188
|
+
` Expected value to be instance of ${expectedType.name || expectedType}\n` +
|
|
189
|
+
` Actual: ${this.actualValue?.constructor?.name || typeof this.actualValue}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
toHaveLength(expectedLength: number) {
|
|
194
|
+
const actualLength = this.actualValue?.length;
|
|
195
|
+
this.triggerResult(actualLength === expectedLength,
|
|
196
|
+
` Expected length: ${expectedLength}\n` +
|
|
197
|
+
` Actual length: ${actualLength}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
93
201
|
toMatch(expectedValue: any) {
|
|
94
202
|
if(typeof this.actualValue.match !== 'function') {
|
|
95
203
|
throw new Error(`You can not use toMatch on type ${typeof this.actualValue}`);
|
|
@@ -132,8 +240,17 @@ class MatcherFactory {
|
|
|
132
240
|
}
|
|
133
241
|
|
|
134
242
|
toContain(needle: any) {
|
|
135
|
-
|
|
136
|
-
|
|
243
|
+
const value = this.actualValue;
|
|
244
|
+
let contains: boolean;
|
|
245
|
+
if (typeof value === 'string') {
|
|
246
|
+
contains = value.includes(String(needle));
|
|
247
|
+
} else if (value instanceof Array) {
|
|
248
|
+
contains = value.indexOf(needle) !== -1;
|
|
249
|
+
} else {
|
|
250
|
+
contains = false;
|
|
251
|
+
}
|
|
252
|
+
this.triggerResult(contains,
|
|
253
|
+
` Expected ` + value + ` to contain ` + needle
|
|
137
254
|
);
|
|
138
255
|
}
|
|
139
256
|
toBeLessThan(greaterValue: number) {
|
|
@@ -146,16 +263,27 @@ class MatcherFactory {
|
|
|
146
263
|
` Expected ` + this.actualValue + ` to be greater than ` + smallerValue
|
|
147
264
|
);
|
|
148
265
|
}
|
|
266
|
+
toBeGreaterThanOrEqual(value: number) {
|
|
267
|
+
this.triggerResult(this.actualValue >= value,
|
|
268
|
+
` Expected ${this.actualValue} to be greater than or equal to ${value}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
toBeLessThanOrEqual(value: number) {
|
|
272
|
+
this.triggerResult(this.actualValue <= value,
|
|
273
|
+
` Expected ${this.actualValue} to be less than or equal to ${value}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
149
276
|
toBeCloseTo(expectedValue: number, precision: number) {
|
|
150
277
|
const shiftHelper = Math.pow(10, precision);
|
|
151
278
|
this.triggerResult(Math.round((this.actualValue as unknown as number) * shiftHelper) / shiftHelper === Math.round(expectedValue * shiftHelper) / shiftHelper,
|
|
152
279
|
` Expected ` + this.actualValue + ` with precision ` + precision + ` to be close to ` + expectedValue
|
|
153
280
|
);
|
|
154
281
|
}
|
|
155
|
-
toThrow(
|
|
156
|
-
let errorMessage = '';
|
|
282
|
+
toThrow(expected?: typeof Error | string | RegExp) {
|
|
283
|
+
let errorMessage = '';
|
|
157
284
|
let didThrow = false;
|
|
158
285
|
let typeMatch = true;
|
|
286
|
+
let messageMatch = true;
|
|
159
287
|
try {
|
|
160
288
|
this.actualValue();
|
|
161
289
|
didThrow = false;
|
|
@@ -163,8 +291,12 @@ class MatcherFactory {
|
|
|
163
291
|
catch(e) {
|
|
164
292
|
errorMessage = e.message || '';
|
|
165
293
|
didThrow = true;
|
|
166
|
-
if(
|
|
167
|
-
typeMatch = (e instanceof
|
|
294
|
+
if (typeof expected === 'function') {
|
|
295
|
+
typeMatch = (e instanceof expected);
|
|
296
|
+
} else if (typeof expected === 'string') {
|
|
297
|
+
messageMatch = errorMessage.includes(expected);
|
|
298
|
+
} else if (expected instanceof RegExp) {
|
|
299
|
+
messageMatch = expected.test(errorMessage);
|
|
168
300
|
}
|
|
169
301
|
}
|
|
170
302
|
const functionName = this.actualValue.name || typeof this.actualValue === 'function' ? "[anonymous function]" : this.actualValue.toString();
|
|
@@ -172,26 +304,122 @@ class MatcherFactory {
|
|
|
172
304
|
` Expected ${functionName} to ${this.positive ? 'throw' : 'not throw'} an exception ${!this.positive && errorMessage ? `, but an error with the message "${errorMessage}" was thrown` : ''}`
|
|
173
305
|
);
|
|
174
306
|
|
|
175
|
-
if(
|
|
307
|
+
if (typeof expected === 'function') {
|
|
176
308
|
this.triggerResult(typeMatch,
|
|
177
|
-
` Expected Error type '${
|
|
309
|
+
` Expected Error type '${expected.name}', but the error is not an instance of it`
|
|
310
|
+
);
|
|
311
|
+
} else if (expected !== undefined) {
|
|
312
|
+
this.triggerResult(messageMatch,
|
|
313
|
+
` Expected error message to match ${expected}\n` +
|
|
314
|
+
` Actual message: "${errorMessage}"`
|
|
178
315
|
);
|
|
179
316
|
}
|
|
180
317
|
}
|
|
318
|
+
|
|
319
|
+
async toReject(expected?: typeof Error | string | RegExp) {
|
|
320
|
+
let didReject = false;
|
|
321
|
+
let errorMessage = '';
|
|
322
|
+
let typeMatch = true;
|
|
323
|
+
let messageMatch = true;
|
|
324
|
+
try {
|
|
325
|
+
await this.actualValue;
|
|
326
|
+
didReject = false;
|
|
327
|
+
} catch (e) {
|
|
328
|
+
didReject = true;
|
|
329
|
+
errorMessage = e?.message || String(e);
|
|
330
|
+
if (typeof expected === 'function') {
|
|
331
|
+
typeMatch = (e instanceof expected);
|
|
332
|
+
} else if (typeof expected === 'string') {
|
|
333
|
+
messageMatch = errorMessage.includes(expected);
|
|
334
|
+
} else if (expected instanceof RegExp) {
|
|
335
|
+
messageMatch = expected.test(errorMessage);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.triggerResult(didReject,
|
|
339
|
+
` Expected promise to ${this.positive ? 'reject' : 'resolve'}${!this.positive && errorMessage ? `, but it rejected with "${errorMessage}"` : ''}`
|
|
340
|
+
);
|
|
341
|
+
if (didReject && typeof expected === 'function') {
|
|
342
|
+
this.triggerResult(typeMatch,
|
|
343
|
+
` Expected rejection type '${expected.name}', but the error is not an instance of it`
|
|
344
|
+
);
|
|
345
|
+
} else if (didReject && expected !== undefined) {
|
|
346
|
+
this.triggerResult(messageMatch,
|
|
347
|
+
` Expected rejection message to match ${expected}\n` +
|
|
348
|
+
` Actual message: "${errorMessage}"`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async toResolve() {
|
|
354
|
+
let didResolve = false;
|
|
355
|
+
let errorMessage = '';
|
|
356
|
+
try {
|
|
357
|
+
await this.actualValue;
|
|
358
|
+
didResolve = true;
|
|
359
|
+
} catch (e) {
|
|
360
|
+
didResolve = false;
|
|
361
|
+
errorMessage = e?.message || String(e);
|
|
362
|
+
}
|
|
363
|
+
this.triggerResult(didResolve,
|
|
364
|
+
` Expected promise to ${this.positive ? 'resolve' : 'reject'}${!didResolve ? `, but it rejected with "${errorMessage}"` : ''}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
181
367
|
}
|
|
182
368
|
|
|
183
|
-
export const describe = async function(moduleName: string, callback: Callback) {
|
|
369
|
+
export const describe = async function(moduleName: string, callback: Callback, options?: { timeout?: number } | number) {
|
|
370
|
+
const suiteTimeoutMs = typeof options === 'number'
|
|
371
|
+
? options
|
|
372
|
+
: (options?.timeout ?? timeoutConfig.suiteTimeout);
|
|
373
|
+
|
|
184
374
|
print('\n' + moduleName);
|
|
185
375
|
|
|
186
|
-
|
|
376
|
+
const t0 = now();
|
|
377
|
+
try {
|
|
378
|
+
await withTimeout(callback, suiteTimeoutMs, `describe: ${moduleName}`);
|
|
379
|
+
} catch (e) {
|
|
380
|
+
if (e instanceof TimeoutError) {
|
|
381
|
+
++countTestsFailed;
|
|
382
|
+
print(` ${RED}⏱ Suite timed out: ${e.message}${RESET}`);
|
|
383
|
+
} else {
|
|
384
|
+
throw e;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const duration = now() - t0;
|
|
388
|
+
print(` ${GRAY}↳ ${formatDuration(duration)}${RESET}`);
|
|
187
389
|
|
|
188
390
|
// Reset after and before callbacks
|
|
189
391
|
beforeEachCb = null;
|
|
190
392
|
afterEachCb = null;
|
|
191
393
|
};
|
|
192
394
|
|
|
395
|
+
describe.skip = async function(moduleName: string, _callback?: Callback) {
|
|
396
|
+
++countTestsIgnored;
|
|
397
|
+
print(`\n${BLUE}- ${moduleName} (skipped)${RESET}`);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const hasDisplay = (): boolean => {
|
|
401
|
+
// Check process.env (Node.js and GJS with @gjsify/globals)
|
|
402
|
+
const env = (globalThis as any).process?.env;
|
|
403
|
+
if (env) {
|
|
404
|
+
return !!(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
405
|
+
}
|
|
406
|
+
// GJS fallback via imports.gi.GLib (before process polyfill is available)
|
|
407
|
+
try {
|
|
408
|
+
const GLib = (globalThis as any)?.imports?.gi?.GLib;
|
|
409
|
+
if (GLib) {
|
|
410
|
+
return !!(GLib.getenv('DISPLAY') || GLib.getenv('WAYLAND_DISPLAY'));
|
|
411
|
+
}
|
|
412
|
+
} catch (_) {}
|
|
413
|
+
return false;
|
|
414
|
+
};
|
|
415
|
+
|
|
193
416
|
const runtimeMatch = async function(onRuntime: Runtime[], version?: string) {
|
|
194
417
|
|
|
418
|
+
// Special case: 'Display' checks for a graphical display, not runtime identity
|
|
419
|
+
if (onRuntime.includes('Display')) {
|
|
420
|
+
return { matched: hasDisplay() };
|
|
421
|
+
}
|
|
422
|
+
|
|
195
423
|
const currRuntime = (await getRuntime());
|
|
196
424
|
|
|
197
425
|
const foundRuntime = onRuntime.find((r) => currRuntime.includes(r));
|
|
@@ -255,27 +483,43 @@ export const afterEach = function (callback?: Callback) {
|
|
|
255
483
|
}
|
|
256
484
|
|
|
257
485
|
|
|
258
|
-
export const it = async function(expectation: string, callback: () => void | Promise<void
|
|
486
|
+
export const it = async function(expectation: string, callback: () => void | Promise<void>, options?: { timeout?: number } | number) {
|
|
487
|
+
const timeoutMs = typeof options === 'number'
|
|
488
|
+
? options
|
|
489
|
+
: (options?.timeout ?? timeoutConfig.testTimeout);
|
|
490
|
+
|
|
491
|
+
const t0 = now();
|
|
259
492
|
try {
|
|
260
493
|
if(typeof beforeEachCb === 'function') {
|
|
261
494
|
await beforeEachCb();
|
|
262
495
|
}
|
|
263
|
-
|
|
264
|
-
await callback
|
|
496
|
+
|
|
497
|
+
await withTimeout(callback, timeoutMs, expectation);
|
|
265
498
|
|
|
266
499
|
if(typeof afterEachCb === 'function') {
|
|
267
500
|
await afterEachCb();
|
|
268
501
|
}
|
|
269
502
|
|
|
270
|
-
|
|
503
|
+
const duration = now() - t0;
|
|
504
|
+
print(` ${GREEN}✔${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
|
|
271
505
|
}
|
|
272
506
|
catch(e) {
|
|
273
|
-
|
|
507
|
+
const duration = now() - t0;
|
|
508
|
+
if (!e.__testFailureCounted) {
|
|
509
|
+
++countTestsFailed;
|
|
510
|
+
}
|
|
511
|
+
const icon = e instanceof TimeoutError ? '⏱' : '❌';
|
|
512
|
+
print(` ${RED}${icon}${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
|
|
274
513
|
print(`${RED}${e.message}${RESET}`);
|
|
275
514
|
if (e.stack) print(e.stack);
|
|
276
515
|
}
|
|
277
516
|
}
|
|
278
517
|
|
|
518
|
+
it.skip = async function(expectation: string, _callback?: () => void | Promise<void>) {
|
|
519
|
+
++countTestsIgnored;
|
|
520
|
+
print(` ${BLUE}-${RESET} ${GRAY}${expectation} (skipped)${RESET}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
279
523
|
export const expect = function(actualValue: any) {
|
|
280
524
|
++countTestsOverall;
|
|
281
525
|
|
|
@@ -291,7 +535,12 @@ export const assert = function(success: any, message?: string | Error) {
|
|
|
291
535
|
++countTestsFailed;
|
|
292
536
|
}
|
|
293
537
|
|
|
294
|
-
|
|
538
|
+
try {
|
|
539
|
+
nodeAssert(success, message);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
(error as any).__testFailureCounted = true;
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
295
544
|
}
|
|
296
545
|
|
|
297
546
|
assert.strictEqual = function<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T {
|
|
@@ -300,7 +549,8 @@ assert.strictEqual = function<T>(actual: unknown, expected: T, message?: string
|
|
|
300
549
|
nodeAssert.strictEqual(actual, expected, message);
|
|
301
550
|
} catch (error) {
|
|
302
551
|
++countTestsFailed;
|
|
303
|
-
|
|
552
|
+
(error as any).__testFailureCounted = true;
|
|
553
|
+
throw error;
|
|
304
554
|
}
|
|
305
555
|
}
|
|
306
556
|
|
|
@@ -324,7 +574,8 @@ assert.deepStrictEqual = function<T>(actual: unknown, expected: T, message?: str
|
|
|
324
574
|
nodeAssert.deepStrictEqual(actual, expected, message);
|
|
325
575
|
} catch (error) {
|
|
326
576
|
++countTestsFailed;
|
|
327
|
-
|
|
577
|
+
(error as any).__testFailureCounted = true;
|
|
578
|
+
throw error;
|
|
328
579
|
}
|
|
329
580
|
}
|
|
330
581
|
|
|
@@ -346,6 +597,8 @@ const runTests = async function(namespaces: Namespaces) {
|
|
|
346
597
|
}
|
|
347
598
|
|
|
348
599
|
const printResult = () => {
|
|
600
|
+
const totalMs = runStartTime > 0 ? now() - runStartTime : 0;
|
|
601
|
+
const durationStr = totalMs > 0 ? ` ${GRAY}(${formatDuration(totalMs)})` : '';
|
|
349
602
|
|
|
350
603
|
if( countTestsIgnored ) {
|
|
351
604
|
// some tests ignored
|
|
@@ -354,11 +607,11 @@ const printResult = () => {
|
|
|
354
607
|
|
|
355
608
|
if( countTestsFailed ) {
|
|
356
609
|
// some tests failed
|
|
357
|
-
print(`\n${RED}❌ ${countTestsFailed} of ${countTestsOverall} tests failed${RESET}`);
|
|
610
|
+
print(`\n${RED}❌ ${countTestsFailed} of ${countTestsOverall} tests failed${durationStr}${RESET}`);
|
|
358
611
|
}
|
|
359
612
|
else {
|
|
360
613
|
// all tests okay
|
|
361
|
-
print(`\n${GREEN}✔ ${countTestsOverall} completed${RESET}`);
|
|
614
|
+
print(`\n${GREEN}✔ ${countTestsOverall} completed${durationStr}${RESET}`);
|
|
362
615
|
}
|
|
363
616
|
}
|
|
364
617
|
|
|
@@ -396,20 +649,62 @@ const printRuntime = async () => {
|
|
|
396
649
|
print(`\nRunning on ${runtime}`);
|
|
397
650
|
}
|
|
398
651
|
|
|
399
|
-
export const run = async (namespaces: Namespaces) => {
|
|
652
|
+
export const run = async (namespaces: Namespaces, options?: { timeout?: number; testTimeout?: number; suiteTimeout?: number } | number) => {
|
|
653
|
+
|
|
654
|
+
applyEnvOverrides();
|
|
655
|
+
runStartTime = now();
|
|
656
|
+
|
|
657
|
+
if (options) {
|
|
658
|
+
if (typeof options === 'number') {
|
|
659
|
+
timeoutConfig.runTimeout = options;
|
|
660
|
+
} else {
|
|
661
|
+
if (options.timeout !== undefined) timeoutConfig.runTimeout = options.timeout;
|
|
662
|
+
if (options.testTimeout !== undefined) timeoutConfig.testTimeout = options.testTimeout;
|
|
663
|
+
if (options.suiteTimeout !== undefined) timeoutConfig.suiteTimeout = options.suiteTimeout;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
400
666
|
|
|
401
667
|
printRuntime()
|
|
402
668
|
.then(async () => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
669
|
+
try {
|
|
670
|
+
await withTimeout(() => runTests(namespaces), timeoutConfig.runTimeout, 'entire test run');
|
|
671
|
+
} catch (e) {
|
|
672
|
+
if (e instanceof TimeoutError) {
|
|
673
|
+
print(`\n${RED}⏱ ${e.message}${RESET}`);
|
|
674
|
+
++countTestsFailed;
|
|
675
|
+
} else {
|
|
676
|
+
throw e;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
.then(async () => {
|
|
681
|
+
printResult();
|
|
682
|
+
print();
|
|
683
|
+
|
|
684
|
+
quitMainLoop(); // Pre-quit ensureMainLoop's loop so it exits immediately when the hook fires
|
|
685
|
+
mainloop?.quit();
|
|
686
|
+
|
|
687
|
+
// Node.js: exit here (code after mainloop?.run() executes before tests on Node.js)
|
|
688
|
+
if (!mainloop) {
|
|
689
|
+
const exitCode = countTestsFailed > 0 ? 1 : 0;
|
|
690
|
+
try {
|
|
691
|
+
const process = globalThis.process || await import('process');
|
|
692
|
+
process.exit(exitCode);
|
|
693
|
+
} catch (_e) { /* process unavailable */ }
|
|
694
|
+
}
|
|
409
695
|
});
|
|
410
696
|
|
|
411
|
-
// Run the GJS mainloop for async operations
|
|
697
|
+
// Run the GJS mainloop for async operations (blocks until mainloop.quit() is called)
|
|
412
698
|
mainloop?.run();
|
|
699
|
+
|
|
700
|
+
// GJS: exit after mainloop returns (system.exit() inside a mainloop
|
|
701
|
+
// callback does not terminate immediately)
|
|
702
|
+
if (mainloop) {
|
|
703
|
+
const exitCode = countTestsFailed > 0 ? 1 : 0;
|
|
704
|
+
try {
|
|
705
|
+
(globalThis as any).imports.system.exit(exitCode);
|
|
706
|
+
} catch (_e) { /* system.exit unavailable */ }
|
|
707
|
+
}
|
|
413
708
|
}
|
|
414
709
|
|
|
415
710
|
export default {
|
|
@@ -421,5 +716,6 @@ export default {
|
|
|
421
716
|
beforeEach,
|
|
422
717
|
on,
|
|
423
718
|
describe,
|
|
424
|
-
|
|
719
|
+
configure,
|
|
720
|
+
print,
|
|
425
721
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
3
4
|
"outDir": "lib",
|
|
4
|
-
"types": [
|
|
5
|
-
|
|
5
|
+
"types": [
|
|
6
|
+
"node"
|
|
7
|
+
],
|
|
8
|
+
"lib": [
|
|
9
|
+
"ESNext"
|
|
10
|
+
],
|
|
6
11
|
"declarationDir": "lib/types",
|
|
7
12
|
"declaration": true,
|
|
13
|
+
"emitDeclarationOnly": true,
|
|
8
14
|
"esModuleInterop": true,
|
|
9
15
|
"target": "ESNext",
|
|
10
|
-
"module": "
|
|
11
|
-
"moduleResolution": "
|
|
16
|
+
"module": "NodeNext",
|
|
17
|
+
"moduleResolution": "NodeNext",
|
|
18
|
+
"strict": false,
|
|
19
|
+
"skipLibCheck": true
|
|
12
20
|
},
|
|
13
|
-
"files": [
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
"files": [
|
|
22
|
+
"src/index.ts"
|
|
23
|
+
],
|
|
24
|
+
"include": [
|
|
25
|
+
"@girs/gjs"
|
|
26
|
+
],
|
|
27
|
+
"exclude": [
|
|
28
|
+
"src/test.ts",
|
|
29
|
+
"src/test.mts",
|
|
30
|
+
"src/**/*.spec.ts",
|
|
31
|
+
"src/**/*.spec.mts"
|
|
32
|
+
]
|
|
33
|
+
}
|