@gjsify/unit 0.0.4 → 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 CHANGED
@@ -1,96 +1,73 @@
1
1
  # @gjsify/unit
2
2
 
3
- A BDD-style testing framework for Gjs, Deno and Node.js, forked from [gjsunit](https://github.com/philipphoffmann/gjsunit).
3
+ Lightweight testing framework for GJS and Node.js. Provides describe, it, expect with cross-platform support.
4
4
 
5
- ## What it is
6
- unit is a BDD-style testing framework for the gjs Javascript binding for Gnome 4x which can be used to write Gnome 4x applications and extensions. It's syntax is totally stolen from [Jasmine](http://jasmine.github.io/) ;-).
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
7
6
 
8
- ## How to use it
9
-
10
- First you need to install the package together with `@gjsify/cli`:
11
-
12
- ```bash
13
- yarn install @gjsify/cli @gjsify/unit -D
14
- ```
15
-
16
- After that you can build and run your tests:
7
+ ## Installation
17
8
 
18
9
  ```bash
19
- gjsify build test-runner.ts --platform gjs --outfile test.gjs.js
20
- gjs -m test.gjs.js
10
+ npm install @gjsify/unit
11
+ # or
12
+ yarn add @gjsify/unit
21
13
  ```
22
14
 
23
- ## Writing test suites
24
-
25
- In your test directory you can have as many subdirectories and test files as you wish. unit will just run all of them.
26
- A test suite could look like this:
27
-
28
- ```js
29
- // my-module.spec.ts
15
+ ## Usage
30
16
 
17
+ ```typescript
31
18
  import { describe, it, expect } from '@gjsify/unit';
32
- import MyModule from './my-module.js';
33
19
 
34
20
  export default async () => {
35
-
36
- await describe('MyModule', async () => {
37
- await it('should say hello', async () => {
38
- var module = new MyModule();
39
-
40
- expect(module.hello()).toEqual('hello');
41
- expect(module.hello()).not.toEqual('hi');
42
- });
43
- });
44
-
45
- }
21
+ await describe('MyModule', async () => {
22
+ await it('should do something', async () => {
23
+ expect(42).toBe(42);
24
+ expect('hello').not.toEqual('world');
25
+ });
26
+ });
27
+ };
46
28
  ```
47
29
 
48
- ```js
49
- // test-runner.ts
30
+ ### Running tests
50
31
 
51
- import { run } from '@gjsify/unit';
52
- import myTestSuite from './my-module.spec.ts';
32
+ ```bash
33
+ # Build and run on GJS
34
+ gjsify build test-runner.ts --platform gjs --outfile test.gjs.js
35
+ gjs -m test.gjs.js
53
36
 
54
- run({myTestSuite});
37
+ # Run on Node.js
38
+ node test-runner.mjs
55
39
  ```
56
40
 
41
+ ### Available matchers
57
42
 
58
- Your test files must expose a function. This is the function you will call in your test runner. In your test suite you can use `describe` and `it` to cluster your test suite. You can then use `expect` to capture the value you want to express expectations on. The available methods for expressing expectations are:
59
- - `toBe(value)` (checks using ===)
60
- - `toEqual(value)` (checks using ==)
61
- - `toMatch(regex)` (checks using String.prototype.match and a regular expression)
62
- - `toBeDefined()` (checks whether the actual value is defined)
63
- - `toBeUndefined()` (opposite of the above)
64
- - `toBeNull()` (checks whether the actual value is null)
65
- - `toBeTruthy()` (checks whether the actual value is castable to true)
66
- - `toBeFalsy()` (checks whether the actual value is castable to false)
67
- - `toContain(needle)` (checks whether an array contains the needle value)
68
- - `toBeLessThan(value)`
69
- - `toBeGreaterThan(value)`
70
- - `toBeCloseTo(value, precision)` (can check float values until a given precision)
71
- - `to(callback)` (checks the value using the provided callback (which gets passed the actual value as first parameter))
43
+ - `toBe(value)` strict equality (`===`)
44
+ - `toEqual(value)` loose equality (`==`)
45
+ - `toMatch(regex)` regex match
46
+ - `toBeDefined()` / `toBeUndefined()`
47
+ - `toBeNull()`
48
+ - `toBeTruthy()` / `toBeFalsy()`
49
+ - `toContain(needle)` array contains
50
+ - `toBeLessThan(value)` / `toBeGreaterThan(value)`
51
+ - `toBeCloseTo(value, precision)` float comparison
52
+ - `toThrow()` expects function to throw
53
+ - `to(callback)` — custom matcher
72
54
 
73
- There is also a `spy` method with which the call of methods can be checked, this is forked from [mysticatea/spy](https://github.com/mysticatea/spy).
55
+ All matchers support `.not` for negation.
74
56
 
75
- ```js
76
- // spy.spec.ts
57
+ ### Spy
77
58
 
78
- import { describe, it, expect, spy } from '@gjsify/unit';
59
+ ```typescript
60
+ import { spy } from '@gjsify/unit';
79
61
 
80
- export default async () => {
81
- await describe("'spy' function", async () => {
82
- await it("should have a calls length of 1 after called one time.", async () => {
83
- const f = spy()
84
- f()
85
-
86
- expect(f.calls.length).toBe(1)
87
- })
88
- })
89
- }
62
+ const f = spy();
63
+ f();
64
+ expect(f.calls.length).toBe(1);
90
65
  ```
91
66
 
92
- I recommend looking at the test suite for examples.
67
+ ## Credits
93
68
 
94
- Happy testing!
69
+ Forked from [gjsunit](https://github.com/philipphoffmann/gjsunit). Spy functionality forked from [mysticatea/spy](https://github.com/mysticatea/spy).
95
70
 
71
+ ## License
96
72
 
73
+ MIT
package/lib/esm/index.js CHANGED
@@ -1,16 +1,67 @@
1
1
  import "@girs/gjs";
2
2
  export * from "./spy.js";
3
- import nodeAssert from "assert";
3
+ import nodeAssert from "node:assert";
4
+ import { quitMainLoop } from "@gjsify/utils/main-loop";
4
5
  const mainloop = globalThis?.imports?.mainloop;
5
6
  let countTestsOverall = 0;
6
7
  let countTestsFailed = 0;
7
8
  let countTestsIgnored = 0;
8
9
  let runtime = "";
10
+ let runStartTime = 0;
11
+ const DEFAULT_TIMEOUT_CONFIG = {
12
+ testTimeout: 5e3,
13
+ suiteTimeout: 3e4,
14
+ runTimeout: 12e4
15
+ };
16
+ let timeoutConfig = { ...DEFAULT_TIMEOUT_CONFIG };
17
+ class TimeoutError extends Error {
18
+ constructor(label, timeoutMs) {
19
+ super(`Timeout: "${label}" exceeded ${timeoutMs}ms`);
20
+ this.name = "TimeoutError";
21
+ }
22
+ }
23
+ async function withTimeout(fn, timeoutMs, label) {
24
+ if (timeoutMs <= 0) return fn();
25
+ let timeoutId;
26
+ const timeoutPromise = new Promise((_, reject) => {
27
+ timeoutId = setTimeout(() => reject(new TimeoutError(label, timeoutMs)), timeoutMs);
28
+ });
29
+ const fnPromise = Promise.resolve(fn());
30
+ fnPromise.catch(() => {
31
+ });
32
+ try {
33
+ return await Promise.race([fnPromise, timeoutPromise]);
34
+ } finally {
35
+ clearTimeout(timeoutId);
36
+ }
37
+ }
38
+ const configure = (overrides) => {
39
+ timeoutConfig = { ...timeoutConfig, ...overrides };
40
+ };
41
+ function applyEnvOverrides() {
42
+ try {
43
+ const env = globalThis.process?.env;
44
+ if (!env) return;
45
+ const t = parseInt(env.GJSIFY_TEST_TIMEOUT, 10);
46
+ if (!isNaN(t) && t >= 0) timeoutConfig.testTimeout = t;
47
+ const s = parseInt(env.GJSIFY_SUITE_TIMEOUT, 10);
48
+ if (!isNaN(s) && s >= 0) timeoutConfig.suiteTimeout = s;
49
+ const r = parseInt(env.GJSIFY_RUN_TIMEOUT, 10);
50
+ if (!isNaN(r) && r >= 0) timeoutConfig.runTimeout = r;
51
+ } catch (_e) {
52
+ }
53
+ }
9
54
  const RED = "\x1B[31m";
10
55
  const GREEN = "\x1B[32m";
11
56
  const BLUE = "\x1B[34m";
12
57
  const GRAY = "\x1B[90m";
13
58
  const RESET = "\x1B[39m";
59
+ const now = () => globalThis.performance?.now?.() ?? Date.now();
60
+ const formatDuration = (ms) => {
61
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
62
+ if (ms >= 100) return `${Math.round(ms)}ms`;
63
+ return `${ms.toFixed(1)}ms`;
64
+ };
14
65
  const print = globalThis.print || console.log;
15
66
  class MatcherFactory {
16
67
  constructor(actualValue, positive, negated) {
@@ -25,8 +76,10 @@ class MatcherFactory {
25
76
  not;
26
77
  triggerResult(success, msg) {
27
78
  if (success && !this.positive || !success && this.positive) {
79
+ const error = new Error(msg);
80
+ error.__testFailureCounted = true;
28
81
  ++countTestsFailed;
29
- throw new Error(msg);
82
+ throw error;
30
83
  }
31
84
  }
32
85
  to(callback) {
@@ -51,14 +104,30 @@ class MatcherFactory {
51
104
  Actual: ${this.actualValue} (${typeof this.actualValue})`
52
105
  );
53
106
  }
107
+ toStrictEqual(expectedValue) {
108
+ let success = true;
109
+ let errorMessage = "";
110
+ try {
111
+ nodeAssert.deepStrictEqual(this.actualValue, expectedValue);
112
+ } catch (e) {
113
+ success = false;
114
+ errorMessage = e.message || "";
115
+ }
116
+ this.triggerResult(
117
+ success,
118
+ ` Expected values to be deeply strictly equal
119
+ Expected: ${JSON.stringify(expectedValue)}
120
+ Actual: ${JSON.stringify(this.actualValue)}` + (errorMessage ? `
121
+ ${errorMessage}` : "")
122
+ );
123
+ }
54
124
  toEqualArray(expectedValue) {
55
125
  let success = Array.isArray(this.actualValue) && Array.isArray(expectedValue) && this.actualValue.length === expectedValue.length;
56
126
  for (let i = 0; i < this.actualValue.length; i++) {
57
127
  const actualVal = this.actualValue[i];
58
128
  const expectedVal = expectedValue[i];
59
129
  success = actualVal == expectedVal;
60
- if (!success)
61
- break;
130
+ if (!success) break;
62
131
  }
63
132
  this.triggerResult(
64
133
  success,
@@ -67,6 +136,21 @@ class MatcherFactory {
67
136
  Actual: ${this.actualValue} (${typeof this.actualValue})`
68
137
  );
69
138
  }
139
+ toBeInstanceOf(expectedType) {
140
+ this.triggerResult(
141
+ this.actualValue instanceof expectedType,
142
+ ` Expected value to be instance of ${expectedType.name || expectedType}
143
+ Actual: ${this.actualValue?.constructor?.name || typeof this.actualValue}`
144
+ );
145
+ }
146
+ toHaveLength(expectedLength) {
147
+ const actualLength = this.actualValue?.length;
148
+ this.triggerResult(
149
+ actualLength === expectedLength,
150
+ ` Expected length: ${expectedLength}
151
+ Actual length: ${actualLength}`
152
+ );
153
+ }
70
154
  toMatch(expectedValue) {
71
155
  if (typeof this.actualValue.match !== "function") {
72
156
  throw new Error(`You can not use toMatch on type ${typeof this.actualValue}`);
@@ -107,9 +191,18 @@ class MatcherFactory {
107
191
  );
108
192
  }
109
193
  toContain(needle) {
194
+ const value = this.actualValue;
195
+ let contains;
196
+ if (typeof value === "string") {
197
+ contains = value.includes(String(needle));
198
+ } else if (value instanceof Array) {
199
+ contains = value.indexOf(needle) !== -1;
200
+ } else {
201
+ contains = false;
202
+ }
110
203
  this.triggerResult(
111
- this.actualValue instanceof Array && this.actualValue.indexOf(needle) !== -1,
112
- ` Expected ` + this.actualValue + ` to contain ` + needle
204
+ contains,
205
+ ` Expected ` + value + ` to contain ` + needle
113
206
  );
114
207
  }
115
208
  toBeLessThan(greaterValue) {
@@ -124,6 +217,18 @@ class MatcherFactory {
124
217
  ` Expected ` + this.actualValue + ` to be greater than ` + smallerValue
125
218
  );
126
219
  }
220
+ toBeGreaterThanOrEqual(value) {
221
+ this.triggerResult(
222
+ this.actualValue >= value,
223
+ ` Expected ${this.actualValue} to be greater than or equal to ${value}`
224
+ );
225
+ }
226
+ toBeLessThanOrEqual(value) {
227
+ this.triggerResult(
228
+ this.actualValue <= value,
229
+ ` Expected ${this.actualValue} to be less than or equal to ${value}`
230
+ );
231
+ }
127
232
  toBeCloseTo(expectedValue, precision) {
128
233
  const shiftHelper = Math.pow(10, precision);
129
234
  this.triggerResult(
@@ -131,18 +236,23 @@ class MatcherFactory {
131
236
  ` Expected ` + this.actualValue + ` with precision ` + precision + ` to be close to ` + expectedValue
132
237
  );
133
238
  }
134
- toThrow(ErrorType) {
239
+ toThrow(expected) {
135
240
  let errorMessage = "";
136
241
  let didThrow = false;
137
242
  let typeMatch = true;
243
+ let messageMatch = true;
138
244
  try {
139
245
  this.actualValue();
140
246
  didThrow = false;
141
247
  } catch (e) {
142
248
  errorMessage = e.message || "";
143
249
  didThrow = true;
144
- if (ErrorType) {
145
- typeMatch = e instanceof ErrorType;
250
+ if (typeof expected === "function") {
251
+ typeMatch = e instanceof expected;
252
+ } else if (typeof expected === "string") {
253
+ messageMatch = errorMessage.includes(expected);
254
+ } else if (expected instanceof RegExp) {
255
+ messageMatch = expected.test(errorMessage);
146
256
  }
147
257
  }
148
258
  const functionName = this.actualValue.name || typeof this.actualValue === "function" ? "[anonymous function]" : this.actualValue.toString();
@@ -150,21 +260,113 @@ class MatcherFactory {
150
260
  didThrow,
151
261
  ` Expected ${functionName} to ${this.positive ? "throw" : "not throw"} an exception ${!this.positive && errorMessage ? `, but an error with the message "${errorMessage}" was thrown` : ""}`
152
262
  );
153
- if (ErrorType) {
263
+ if (typeof expected === "function") {
264
+ this.triggerResult(
265
+ typeMatch,
266
+ ` Expected Error type '${expected.name}', but the error is not an instance of it`
267
+ );
268
+ } else if (expected !== void 0) {
269
+ this.triggerResult(
270
+ messageMatch,
271
+ ` Expected error message to match ${expected}
272
+ Actual message: "${errorMessage}"`
273
+ );
274
+ }
275
+ }
276
+ async toReject(expected) {
277
+ let didReject = false;
278
+ let errorMessage = "";
279
+ let typeMatch = true;
280
+ let messageMatch = true;
281
+ try {
282
+ await this.actualValue;
283
+ didReject = false;
284
+ } catch (e) {
285
+ didReject = true;
286
+ errorMessage = e?.message || String(e);
287
+ if (typeof expected === "function") {
288
+ typeMatch = e instanceof expected;
289
+ } else if (typeof expected === "string") {
290
+ messageMatch = errorMessage.includes(expected);
291
+ } else if (expected instanceof RegExp) {
292
+ messageMatch = expected.test(errorMessage);
293
+ }
294
+ }
295
+ this.triggerResult(
296
+ didReject,
297
+ ` Expected promise to ${this.positive ? "reject" : "resolve"}${!this.positive && errorMessage ? `, but it rejected with "${errorMessage}"` : ""}`
298
+ );
299
+ if (didReject && typeof expected === "function") {
154
300
  this.triggerResult(
155
301
  typeMatch,
156
- ` Expected Error type '${ErrorType.name}', but the error is not an instance of it`
302
+ ` Expected rejection type '${expected.name}', but the error is not an instance of it`
303
+ );
304
+ } else if (didReject && expected !== void 0) {
305
+ this.triggerResult(
306
+ messageMatch,
307
+ ` Expected rejection message to match ${expected}
308
+ Actual message: "${errorMessage}"`
157
309
  );
158
310
  }
159
311
  }
312
+ async toResolve() {
313
+ let didResolve = false;
314
+ let errorMessage = "";
315
+ try {
316
+ await this.actualValue;
317
+ didResolve = true;
318
+ } catch (e) {
319
+ didResolve = false;
320
+ errorMessage = e?.message || String(e);
321
+ }
322
+ this.triggerResult(
323
+ didResolve,
324
+ ` Expected promise to ${this.positive ? "resolve" : "reject"}${!didResolve ? `, but it rejected with "${errorMessage}"` : ""}`
325
+ );
326
+ }
160
327
  }
161
- const describe = async function(moduleName, callback) {
328
+ const describe = async function(moduleName, callback, options) {
329
+ const suiteTimeoutMs = typeof options === "number" ? options : options?.timeout ?? timeoutConfig.suiteTimeout;
162
330
  print("\n" + moduleName);
163
- await callback();
331
+ const t0 = now();
332
+ try {
333
+ await withTimeout(callback, suiteTimeoutMs, `describe: ${moduleName}`);
334
+ } catch (e) {
335
+ if (e instanceof TimeoutError) {
336
+ ++countTestsFailed;
337
+ print(` ${RED}\u23F1 Suite timed out: ${e.message}${RESET}`);
338
+ } else {
339
+ throw e;
340
+ }
341
+ }
342
+ const duration = now() - t0;
343
+ print(` ${GRAY}\u21B3 ${formatDuration(duration)}${RESET}`);
164
344
  beforeEachCb = null;
165
345
  afterEachCb = null;
166
346
  };
347
+ describe.skip = async function(moduleName, _callback) {
348
+ ++countTestsIgnored;
349
+ print(`
350
+ ${BLUE}- ${moduleName} (skipped)${RESET}`);
351
+ };
352
+ const hasDisplay = () => {
353
+ const env = globalThis.process?.env;
354
+ if (env) {
355
+ return !!(env.DISPLAY || env.WAYLAND_DISPLAY);
356
+ }
357
+ try {
358
+ const GLib = globalThis?.imports?.gi?.GLib;
359
+ if (GLib) {
360
+ return !!(GLib.getenv("DISPLAY") || GLib.getenv("WAYLAND_DISPLAY"));
361
+ }
362
+ } catch (_) {
363
+ }
364
+ return false;
365
+ };
167
366
  const runtimeMatch = async function(onRuntime, version) {
367
+ if (onRuntime.includes("Display")) {
368
+ return { matched: hasDisplay() };
369
+ }
168
370
  const currRuntime = await getRuntime();
169
371
  const foundRuntime = onRuntime.find((r) => currRuntime.includes(r));
170
372
  if (!foundRuntime) {
@@ -210,23 +412,34 @@ const beforeEach = function(callback) {
210
412
  const afterEach = function(callback) {
211
413
  afterEachCb = callback;
212
414
  };
213
- const it = async function(expectation, callback) {
415
+ const it = async function(expectation, callback, options) {
416
+ const timeoutMs = typeof options === "number" ? options : options?.timeout ?? timeoutConfig.testTimeout;
417
+ const t0 = now();
214
418
  try {
215
419
  if (typeof beforeEachCb === "function") {
216
420
  await beforeEachCb();
217
421
  }
218
- await callback();
422
+ await withTimeout(callback, timeoutMs, expectation);
219
423
  if (typeof afterEachCb === "function") {
220
424
  await afterEachCb();
221
425
  }
222
- print(` ${GREEN}\u2714${RESET} ${GRAY}${expectation}${RESET}`);
426
+ const duration = now() - t0;
427
+ print(` ${GREEN}\u2714${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
223
428
  } catch (e) {
224
- print(` ${RED}\u274C${RESET} ${GRAY}${expectation}${RESET}`);
429
+ const duration = now() - t0;
430
+ if (!e.__testFailureCounted) {
431
+ ++countTestsFailed;
432
+ }
433
+ const icon = e instanceof TimeoutError ? "\u23F1" : "\u274C";
434
+ print(` ${RED}${icon}${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
225
435
  print(`${RED}${e.message}${RESET}`);
226
- if (e.stack)
227
- print(e.stack);
436
+ if (e.stack) print(e.stack);
228
437
  }
229
438
  };
439
+ it.skip = async function(expectation, _callback) {
440
+ ++countTestsIgnored;
441
+ print(` ${BLUE}-${RESET} ${GRAY}${expectation} (skipped)${RESET}`);
442
+ };
230
443
  const expect = function(actualValue) {
231
444
  ++countTestsOverall;
232
445
  const expecter = new MatcherFactory(actualValue, true);
@@ -237,7 +450,12 @@ const assert = function(success, message) {
237
450
  if (!success) {
238
451
  ++countTestsFailed;
239
452
  }
240
- nodeAssert(success, message);
453
+ try {
454
+ nodeAssert(success, message);
455
+ } catch (error) {
456
+ error.__testFailureCounted = true;
457
+ throw error;
458
+ }
241
459
  };
242
460
  assert.strictEqual = function(actual, expected, message) {
243
461
  ++countTestsOverall;
@@ -245,6 +463,7 @@ assert.strictEqual = function(actual, expected, message) {
245
463
  nodeAssert.strictEqual(actual, expected, message);
246
464
  } catch (error) {
247
465
  ++countTestsFailed;
466
+ error.__testFailureCounted = true;
248
467
  throw error;
249
468
  }
250
469
  };
@@ -256,11 +475,9 @@ assert.throws = function(promiseFn, ...args) {
256
475
  } catch (e) {
257
476
  error = e;
258
477
  }
259
- if (!error)
260
- ++countTestsFailed;
478
+ if (!error) ++countTestsFailed;
261
479
  nodeAssert.throws(() => {
262
- if (error)
263
- throw error;
480
+ if (error) throw error;
264
481
  }, args[0], args[1]);
265
482
  };
266
483
  assert.deepStrictEqual = function(actual, expected, message) {
@@ -269,6 +486,7 @@ assert.deepStrictEqual = function(actual, expected, message) {
269
486
  nodeAssert.deepStrictEqual(actual, expected, message);
270
487
  } catch (error) {
271
488
  ++countTestsFailed;
489
+ error.__testFailureCounted = true;
272
490
  throw error;
273
491
  }
274
492
  };
@@ -283,16 +501,18 @@ const runTests = async function(namespaces) {
283
501
  }
284
502
  };
285
503
  const printResult = () => {
504
+ const totalMs = runStartTime > 0 ? now() - runStartTime : 0;
505
+ const durationStr = totalMs > 0 ? ` ${GRAY}(${formatDuration(totalMs)})` : "";
286
506
  if (countTestsIgnored) {
287
507
  print(`
288
508
  ${BLUE}\u2714 ${countTestsIgnored} ignored test${countTestsIgnored > 1 ? "s" : ""}${RESET}`);
289
509
  }
290
510
  if (countTestsFailed) {
291
511
  print(`
292
- ${RED}\u274C ${countTestsFailed} of ${countTestsOverall} tests failed${RESET}`);
512
+ ${RED}\u274C ${countTestsFailed} of ${countTestsOverall} tests failed${durationStr}${RESET}`);
293
513
  } else {
294
514
  print(`
295
- ${GREEN}\u2714 ${countTestsOverall} completed${RESET}`);
515
+ ${GREEN}\u2714 ${countTestsOverall} completed${durationStr}${RESET}`);
296
516
  }
297
517
  };
298
518
  const getRuntime = async () => {
@@ -325,17 +545,54 @@ const printRuntime = async () => {
325
545
  print(`
326
546
  Running on ${runtime2}`);
327
547
  };
328
- const run = async (namespaces) => {
548
+ const run = async (namespaces, options) => {
549
+ applyEnvOverrides();
550
+ runStartTime = now();
551
+ if (options) {
552
+ if (typeof options === "number") {
553
+ timeoutConfig.runTimeout = options;
554
+ } else {
555
+ if (options.timeout !== void 0) timeoutConfig.runTimeout = options.timeout;
556
+ if (options.testTimeout !== void 0) timeoutConfig.testTimeout = options.testTimeout;
557
+ if (options.suiteTimeout !== void 0) timeoutConfig.suiteTimeout = options.suiteTimeout;
558
+ }
559
+ }
329
560
  printRuntime().then(async () => {
330
- return runTests(namespaces).then(() => {
331
- printResult();
332
- print();
333
- mainloop?.quit();
334
- });
561
+ try {
562
+ await withTimeout(() => runTests(namespaces), timeoutConfig.runTimeout, "entire test run");
563
+ } catch (e) {
564
+ if (e instanceof TimeoutError) {
565
+ print(`
566
+ ${RED}\u23F1 ${e.message}${RESET}`);
567
+ ++countTestsFailed;
568
+ } else {
569
+ throw e;
570
+ }
571
+ }
572
+ }).then(async () => {
573
+ printResult();
574
+ print();
575
+ quitMainLoop();
576
+ mainloop?.quit();
577
+ if (!mainloop) {
578
+ const exitCode = countTestsFailed > 0 ? 1 : 0;
579
+ try {
580
+ const process = globalThis.process || await import("process");
581
+ process.exit(exitCode);
582
+ } catch (_e) {
583
+ }
584
+ }
335
585
  });
336
586
  mainloop?.run();
587
+ if (mainloop) {
588
+ const exitCode = countTestsFailed > 0 ? 1 : 0;
589
+ try {
590
+ globalThis.imports.system.exit(exitCode);
591
+ } catch (_e) {
592
+ }
593
+ }
337
594
  };
338
- var src_default = {
595
+ var index_default = {
339
596
  run,
340
597
  assert,
341
598
  expect,
@@ -344,13 +601,15 @@ var src_default = {
344
601
  beforeEach,
345
602
  on,
346
603
  describe,
604
+ configure,
347
605
  print
348
606
  };
349
607
  export {
350
608
  afterEach,
351
609
  assert,
352
610
  beforeEach,
353
- src_default as default,
611
+ configure,
612
+ index_default as default,
354
613
  describe,
355
614
  expect,
356
615
  it,