@gjsify/unit 0.3.21 → 0.4.3
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/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +1 -1
- package/lib/esm/spy.js +1 -1
- package/package.json +63 -53
- package/src/index.spec.ts +0 -308
- package/src/index.ts +0 -760
- package/src/spy.spec.ts +0 -641
- package/src/spy.ts +0 -246
- package/src/test.mts +0 -5
- package/tsconfig.json +0 -31
package/src/index.ts
DELETED
|
@@ -1,760 +0,0 @@
|
|
|
1
|
-
// Based on https://github.com/philipphoffmann/gjsunit
|
|
2
|
-
|
|
3
|
-
import "@girs/gjs";
|
|
4
|
-
|
|
5
|
-
import type GLib from '@girs/glib-2.0';
|
|
6
|
-
export * from './spy.js';
|
|
7
|
-
import nodeAssert from 'node:assert';
|
|
8
|
-
import { quitMainLoop } from '@gjsify/utils/main-loop';
|
|
9
|
-
|
|
10
|
-
const mainloop: GLib.MainLoop | undefined = (globalThis as any)?.imports?.mainloop;
|
|
11
|
-
|
|
12
|
-
let countTestsOverall = 0;
|
|
13
|
-
let countTestsFailed = 0;
|
|
14
|
-
let countTestsIgnored = 0;
|
|
15
|
-
let runtime = '';
|
|
16
|
-
let runStartTime = 0;
|
|
17
|
-
let currentSuite = '';
|
|
18
|
-
let testErrors: Array<{ suite: string; test: string; message: string }> = [];
|
|
19
|
-
|
|
20
|
-
export interface TimeoutConfig {
|
|
21
|
-
/** Per-it() timeout in ms. Default: 5000. 0 = disabled. */
|
|
22
|
-
testTimeout: number;
|
|
23
|
-
/** Per-describe() timeout in ms. Default: 30000. 0 = disabled. */
|
|
24
|
-
suiteTimeout: number;
|
|
25
|
-
/** Global run timeout in ms. Default: 120000. 0 = disabled. */
|
|
26
|
-
runTimeout: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const DEFAULT_TIMEOUT_CONFIG: TimeoutConfig = {
|
|
30
|
-
testTimeout: 5000,
|
|
31
|
-
suiteTimeout: 30000,
|
|
32
|
-
runTimeout: 120000,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
let timeoutConfig: TimeoutConfig = { ...DEFAULT_TIMEOUT_CONFIG };
|
|
36
|
-
|
|
37
|
-
class TimeoutError extends Error {
|
|
38
|
-
constructor(label: string, timeoutMs: number) {
|
|
39
|
-
super(`Timeout: "${label}" exceeded ${timeoutMs}ms`);
|
|
40
|
-
this.name = 'TimeoutError';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function withTimeout<T>(
|
|
45
|
-
fn: () => T | Promise<T>,
|
|
46
|
-
timeoutMs: number,
|
|
47
|
-
label: string
|
|
48
|
-
): Promise<T> {
|
|
49
|
-
if (timeoutMs <= 0) return fn();
|
|
50
|
-
|
|
51
|
-
let timeoutId: ReturnType<typeof setTimeout>;
|
|
52
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
53
|
-
timeoutId = setTimeout(() => reject(new TimeoutError(label, timeoutMs)), timeoutMs);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const fnPromise = Promise.resolve(fn());
|
|
57
|
-
fnPromise.catch(() => {}); // Prevent unhandled rejection if it fails after timeout
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
return await Promise.race([fnPromise, timeoutPromise]);
|
|
61
|
-
} finally {
|
|
62
|
-
clearTimeout(timeoutId!);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const configure = (overrides: Partial<TimeoutConfig>) => {
|
|
67
|
-
timeoutConfig = { ...timeoutConfig, ...overrides };
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function applyEnvOverrides() {
|
|
71
|
-
try {
|
|
72
|
-
const env = (globalThis as any).process?.env;
|
|
73
|
-
if (!env) return;
|
|
74
|
-
const t = parseInt(env.GJSIFY_TEST_TIMEOUT, 10);
|
|
75
|
-
if (!isNaN(t) && t >= 0) timeoutConfig.testTimeout = t;
|
|
76
|
-
const s = parseInt(env.GJSIFY_SUITE_TIMEOUT, 10);
|
|
77
|
-
if (!isNaN(s) && s >= 0) timeoutConfig.suiteTimeout = s;
|
|
78
|
-
const r = parseInt(env.GJSIFY_RUN_TIMEOUT, 10);
|
|
79
|
-
if (!isNaN(r) && r >= 0) timeoutConfig.runTimeout = r;
|
|
80
|
-
} catch (_e) { /* process.env may not be available */ }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const RED = '\x1B[31m';
|
|
84
|
-
const GREEN = '\x1B[32m';
|
|
85
|
-
const BLUE = '\x1b[34m';
|
|
86
|
-
const GRAY = '\x1B[90m';
|
|
87
|
-
const RESET = '\x1B[39m';
|
|
88
|
-
|
|
89
|
-
const now = (): number =>
|
|
90
|
-
(globalThis as any).performance?.now?.() ?? Date.now();
|
|
91
|
-
|
|
92
|
-
const formatDuration = (ms: number): string => {
|
|
93
|
-
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
94
|
-
if (ms >= 100) return `${Math.round(ms)}ms`;
|
|
95
|
-
return `${ms.toFixed(1)}ms`;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export interface Namespaces {
|
|
99
|
-
[key: string]: () => (Promise<void>) | Namespaces;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type Callback = () => Promise<void>;
|
|
103
|
-
|
|
104
|
-
export type Runtime = 'Gjs' | 'Deno' | 'Node.js' | 'Unknown' | 'Browser' | 'Display';
|
|
105
|
-
|
|
106
|
-
// Makes this work on Gjs and Node.js
|
|
107
|
-
// In browsers, globalThis.print is window.print() (the print dialog), not text output.
|
|
108
|
-
// Use console.log in browser contexts to avoid triggering print dialogs.
|
|
109
|
-
// GJS check takes priority: @gjsify/dom-elements can set globalThis.document on GJS,
|
|
110
|
-
// which would otherwise cause a false-positive browser detection.
|
|
111
|
-
const _isGjsProcess = typeof (globalThis as any).process?.versions?.gjs === 'string';
|
|
112
|
-
export const print = (!_isGjsProcess && typeof (globalThis as any).document !== 'undefined')
|
|
113
|
-
? console.log
|
|
114
|
-
: (globalThis.print || console.log);
|
|
115
|
-
|
|
116
|
-
class MatcherFactory {
|
|
117
|
-
|
|
118
|
-
public not: MatcherFactory;
|
|
119
|
-
|
|
120
|
-
constructor(protected readonly actualValue: any, protected readonly positive: boolean, negated?: MatcherFactory) {
|
|
121
|
-
if(negated) {
|
|
122
|
-
this.not = negated;
|
|
123
|
-
} else {
|
|
124
|
-
this.not = new MatcherFactory(actualValue, !positive, this);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
triggerResult(success: boolean, msg: string) {
|
|
129
|
-
if( (success && !this.positive) ||
|
|
130
|
-
(!success && this.positive) ) {
|
|
131
|
-
const error = new Error(msg);
|
|
132
|
-
(error as any).__testFailureCounted = true;
|
|
133
|
-
++countTestsFailed;
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
to(callback: (actualValue: any) => boolean) {
|
|
139
|
-
this.triggerResult(callback(this.actualValue),
|
|
140
|
-
` Expected callback to validate`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
toBe(expectedValue: any) {
|
|
145
|
-
this.triggerResult(this.actualValue === expectedValue,
|
|
146
|
-
` Expected values to match using ===\n` +
|
|
147
|
-
` Expected: ${expectedValue} (${typeof expectedValue})\n` +
|
|
148
|
-
` Actual: ${this.actualValue} (${typeof this.actualValue})`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
toEqual(expectedValue: any) {
|
|
153
|
-
this.triggerResult(this.actualValue == expectedValue,
|
|
154
|
-
` Expected values to match using ==\n` +
|
|
155
|
-
` Expected: ${expectedValue} (${typeof expectedValue})\n` +
|
|
156
|
-
` Actual: ${this.actualValue} (${typeof this.actualValue})`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
toStrictEqual(expectedValue: any) {
|
|
161
|
-
let success = true;
|
|
162
|
-
let errorMessage = '';
|
|
163
|
-
try {
|
|
164
|
-
nodeAssert.deepStrictEqual(this.actualValue, expectedValue);
|
|
165
|
-
} catch (e) {
|
|
166
|
-
success = false;
|
|
167
|
-
errorMessage = e.message || '';
|
|
168
|
-
}
|
|
169
|
-
this.triggerResult(success,
|
|
170
|
-
` Expected values to be deeply strictly equal\n` +
|
|
171
|
-
` Expected: ${JSON.stringify(expectedValue)}\n` +
|
|
172
|
-
` Actual: ${JSON.stringify(this.actualValue)}` +
|
|
173
|
-
(errorMessage ? `\n ${errorMessage}` : '')
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
toEqualArray(expectedValue: Array<any> | Uint8Array) {
|
|
178
|
-
|
|
179
|
-
let success = Array.isArray(this.actualValue) && Array.isArray(expectedValue) && this.actualValue.length === expectedValue.length;
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < this.actualValue.length; i++) {
|
|
182
|
-
const actualVal = this.actualValue[i];
|
|
183
|
-
const expectedVal = expectedValue[i];
|
|
184
|
-
success = actualVal == expectedVal;
|
|
185
|
-
if(!success) break;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.triggerResult(success,
|
|
189
|
-
` Expected array items to match using ==\n` +
|
|
190
|
-
` Expected: ${expectedValue} (${typeof expectedValue})\n` +
|
|
191
|
-
` Actual: ${this.actualValue} (${typeof this.actualValue})`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
toBeInstanceOf(expectedType: Function) {
|
|
196
|
-
this.triggerResult(this.actualValue instanceof expectedType,
|
|
197
|
-
` Expected value to be instance of ${expectedType.name || expectedType}\n` +
|
|
198
|
-
` Actual: ${this.actualValue?.constructor?.name || typeof this.actualValue}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
toHaveLength(expectedLength: number) {
|
|
203
|
-
const actualLength = this.actualValue?.length;
|
|
204
|
-
this.triggerResult(actualLength === expectedLength,
|
|
205
|
-
` Expected length: ${expectedLength}\n` +
|
|
206
|
-
` Actual length: ${actualLength}`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
toMatch(expectedValue: any) {
|
|
211
|
-
if(typeof this.actualValue.match !== 'function') {
|
|
212
|
-
throw new Error(`You can not use toMatch on type ${typeof this.actualValue}`);
|
|
213
|
-
}
|
|
214
|
-
this.triggerResult(!!this.actualValue.match(expectedValue),
|
|
215
|
-
' Expected values to match using regular expression\n' +
|
|
216
|
-
' Expression: ' + expectedValue + '\n' +
|
|
217
|
-
' Actual: ' + this.actualValue
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
toBeDefined() {
|
|
222
|
-
this.triggerResult(typeof this.actualValue !== 'undefined',
|
|
223
|
-
` Expected value to be defined`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
toBeUndefined() {
|
|
228
|
-
this.triggerResult(typeof this.actualValue === 'undefined',
|
|
229
|
-
` Expected value to be undefined`
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
toBeNull() {
|
|
234
|
-
this.triggerResult(this.actualValue === null,
|
|
235
|
-
` Expected value to be null`
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
toBeTruthy() {
|
|
240
|
-
this.triggerResult(this.actualValue as unknown as boolean,
|
|
241
|
-
` Expected value to be truthy`
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
toBeFalsy() {
|
|
246
|
-
this.triggerResult(!this.actualValue,
|
|
247
|
-
` Expected value to be falsy`
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
toContain(needle: any) {
|
|
252
|
-
const value = this.actualValue;
|
|
253
|
-
let contains: boolean;
|
|
254
|
-
if (typeof value === 'string') {
|
|
255
|
-
contains = value.includes(String(needle));
|
|
256
|
-
} else if (value instanceof Array) {
|
|
257
|
-
contains = value.indexOf(needle) !== -1;
|
|
258
|
-
} else {
|
|
259
|
-
contains = false;
|
|
260
|
-
}
|
|
261
|
-
this.triggerResult(contains,
|
|
262
|
-
` Expected ` + value + ` to contain ` + needle
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
toBeLessThan(greaterValue: number) {
|
|
266
|
-
this.triggerResult(this.actualValue < greaterValue,
|
|
267
|
-
` Expected ` + this.actualValue + ` to be less than ` + greaterValue
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
toBeGreaterThan(smallerValue: number) {
|
|
271
|
-
this.triggerResult(this.actualValue > smallerValue,
|
|
272
|
-
` Expected ` + this.actualValue + ` to be greater than ` + smallerValue
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
toBeGreaterThanOrEqual(value: number) {
|
|
276
|
-
this.triggerResult(this.actualValue >= value,
|
|
277
|
-
` Expected ${this.actualValue} to be greater than or equal to ${value}`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
toBeLessThanOrEqual(value: number) {
|
|
281
|
-
this.triggerResult(this.actualValue <= value,
|
|
282
|
-
` Expected ${this.actualValue} to be less than or equal to ${value}`
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
toBeCloseTo(expectedValue: number, precision: number) {
|
|
286
|
-
const shiftHelper = Math.pow(10, precision);
|
|
287
|
-
this.triggerResult(Math.round((this.actualValue as unknown as number) * shiftHelper) / shiftHelper === Math.round(expectedValue * shiftHelper) / shiftHelper,
|
|
288
|
-
` Expected ` + this.actualValue + ` with precision ` + precision + ` to be close to ` + expectedValue
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
toThrow(expected?: typeof Error | string | RegExp) {
|
|
292
|
-
let errorMessage = '';
|
|
293
|
-
let didThrow = false;
|
|
294
|
-
let typeMatch = true;
|
|
295
|
-
let messageMatch = true;
|
|
296
|
-
try {
|
|
297
|
-
this.actualValue();
|
|
298
|
-
didThrow = false;
|
|
299
|
-
}
|
|
300
|
-
catch(e) {
|
|
301
|
-
errorMessage = e.message || '';
|
|
302
|
-
didThrow = true;
|
|
303
|
-
if (typeof expected === 'function') {
|
|
304
|
-
typeMatch = (e instanceof expected);
|
|
305
|
-
} else if (typeof expected === 'string') {
|
|
306
|
-
messageMatch = errorMessage.includes(expected);
|
|
307
|
-
} else if (expected instanceof RegExp) {
|
|
308
|
-
messageMatch = expected.test(errorMessage);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const functionName = this.actualValue.name || typeof this.actualValue === 'function' ? "[anonymous function]" : this.actualValue.toString();
|
|
312
|
-
this.triggerResult(didThrow,
|
|
313
|
-
` Expected ${functionName} to ${this.positive ? 'throw' : 'not throw'} an exception ${!this.positive && errorMessage ? `, but an error with the message "${errorMessage}" was thrown` : ''}`
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
if (typeof expected === 'function') {
|
|
317
|
-
this.triggerResult(typeMatch,
|
|
318
|
-
` Expected Error type '${expected.name}', but the error is not an instance of it`
|
|
319
|
-
);
|
|
320
|
-
} else if (expected !== undefined) {
|
|
321
|
-
this.triggerResult(messageMatch,
|
|
322
|
-
` Expected error message to match ${expected}\n` +
|
|
323
|
-
` Actual message: "${errorMessage}"`
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async toReject(expected?: typeof Error | string | RegExp) {
|
|
329
|
-
let didReject = false;
|
|
330
|
-
let errorMessage = '';
|
|
331
|
-
let typeMatch = true;
|
|
332
|
-
let messageMatch = true;
|
|
333
|
-
try {
|
|
334
|
-
await this.actualValue;
|
|
335
|
-
didReject = false;
|
|
336
|
-
} catch (e) {
|
|
337
|
-
didReject = true;
|
|
338
|
-
errorMessage = e?.message || String(e);
|
|
339
|
-
if (typeof expected === 'function') {
|
|
340
|
-
typeMatch = (e instanceof expected);
|
|
341
|
-
} else if (typeof expected === 'string') {
|
|
342
|
-
messageMatch = errorMessage.includes(expected);
|
|
343
|
-
} else if (expected instanceof RegExp) {
|
|
344
|
-
messageMatch = expected.test(errorMessage);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
this.triggerResult(didReject,
|
|
348
|
-
` Expected promise to ${this.positive ? 'reject' : 'resolve'}${!this.positive && errorMessage ? `, but it rejected with "${errorMessage}"` : ''}`
|
|
349
|
-
);
|
|
350
|
-
if (didReject && typeof expected === 'function') {
|
|
351
|
-
this.triggerResult(typeMatch,
|
|
352
|
-
` Expected rejection type '${expected.name}', but the error is not an instance of it`
|
|
353
|
-
);
|
|
354
|
-
} else if (didReject && expected !== undefined) {
|
|
355
|
-
this.triggerResult(messageMatch,
|
|
356
|
-
` Expected rejection message to match ${expected}\n` +
|
|
357
|
-
` Actual message: "${errorMessage}"`
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async toResolve() {
|
|
363
|
-
let didResolve = false;
|
|
364
|
-
let errorMessage = '';
|
|
365
|
-
try {
|
|
366
|
-
await this.actualValue;
|
|
367
|
-
didResolve = true;
|
|
368
|
-
} catch (e) {
|
|
369
|
-
didResolve = false;
|
|
370
|
-
errorMessage = e?.message || String(e);
|
|
371
|
-
}
|
|
372
|
-
this.triggerResult(didResolve,
|
|
373
|
-
` Expected promise to ${this.positive ? 'resolve' : 'reject'}${!didResolve ? `, but it rejected with "${errorMessage}"` : ''}`
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export const describe = async function(moduleName: string, callback: Callback, options?: { timeout?: number } | number) {
|
|
379
|
-
const suiteTimeoutMs = typeof options === 'number'
|
|
380
|
-
? options
|
|
381
|
-
: (options?.timeout ?? timeoutConfig.suiteTimeout);
|
|
382
|
-
|
|
383
|
-
print('\n' + moduleName);
|
|
384
|
-
|
|
385
|
-
const prevSuite = currentSuite;
|
|
386
|
-
currentSuite = moduleName;
|
|
387
|
-
const t0 = now();
|
|
388
|
-
try {
|
|
389
|
-
await withTimeout(callback, suiteTimeoutMs, `describe: ${moduleName}`);
|
|
390
|
-
} catch (e) {
|
|
391
|
-
if (e instanceof TimeoutError) {
|
|
392
|
-
++countTestsFailed;
|
|
393
|
-
print(` ${RED}⏱ Suite timed out: ${e.message}${RESET}`);
|
|
394
|
-
} else {
|
|
395
|
-
throw e;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
currentSuite = prevSuite;
|
|
399
|
-
const duration = now() - t0;
|
|
400
|
-
print(` ${GRAY}↳ ${formatDuration(duration)}${RESET}`);
|
|
401
|
-
|
|
402
|
-
// Reset after and before callbacks
|
|
403
|
-
beforeEachCb = null;
|
|
404
|
-
afterEachCb = null;
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
describe.skip = async function(moduleName: string, _callback?: Callback) {
|
|
408
|
-
++countTestsIgnored;
|
|
409
|
-
print(`\n${BLUE}- ${moduleName} (skipped)${RESET}`);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const hasDisplay = (): boolean => {
|
|
413
|
-
// Check process.env (Node.js and GJS with @gjsify/globals)
|
|
414
|
-
const env = (globalThis as any).process?.env;
|
|
415
|
-
if (env) {
|
|
416
|
-
return !!(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
417
|
-
}
|
|
418
|
-
// GJS fallback via imports.gi.GLib (before process polyfill is available)
|
|
419
|
-
try {
|
|
420
|
-
const GLib = (globalThis as any)?.imports?.gi?.GLib;
|
|
421
|
-
if (GLib) {
|
|
422
|
-
return !!(GLib.getenv('DISPLAY') || GLib.getenv('WAYLAND_DISPLAY'));
|
|
423
|
-
}
|
|
424
|
-
} catch (_) {}
|
|
425
|
-
return false;
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const runtimeMatch = async function(onRuntime: Runtime[], version?: string) {
|
|
429
|
-
|
|
430
|
-
// Special case: 'Display' checks for a graphical display, not runtime identity
|
|
431
|
-
if (onRuntime.includes('Display')) {
|
|
432
|
-
return { matched: hasDisplay() };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const currRuntime = (await getRuntime());
|
|
436
|
-
|
|
437
|
-
const foundRuntime = onRuntime.find((r) => currRuntime.includes(r));
|
|
438
|
-
|
|
439
|
-
if (!foundRuntime) {
|
|
440
|
-
return {
|
|
441
|
-
matched: false
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if(typeof version === 'string') {
|
|
446
|
-
// TODO allow version wildcards like 16.x.x
|
|
447
|
-
if(!currRuntime.includes(version)) {
|
|
448
|
-
return {
|
|
449
|
-
matched: false
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
matched: true,
|
|
456
|
-
runtime: foundRuntime,
|
|
457
|
-
version: version,
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// TODO add support for Browser
|
|
462
|
-
/** E.g on('Deno', () { it(...) }) */
|
|
463
|
-
export const on = async function(onRuntime: Runtime | Runtime[], version: string | Callback, callback?: Callback) {
|
|
464
|
-
|
|
465
|
-
if(typeof onRuntime === 'string') {
|
|
466
|
-
onRuntime = [onRuntime];
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if(typeof version === 'function') {
|
|
470
|
-
callback = version;
|
|
471
|
-
version = undefined;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const { matched } = await runtimeMatch(onRuntime, version as string | undefined);
|
|
475
|
-
|
|
476
|
-
if(!matched) {
|
|
477
|
-
++countTestsIgnored;
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
print(`\nOn ${onRuntime.join(', ')}${version ? ' ' + version : ''}`);
|
|
482
|
-
|
|
483
|
-
await callback();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
let beforeEachCb: Callback | undefined | null;
|
|
487
|
-
let afterEachCb: Callback | undefined | null;
|
|
488
|
-
|
|
489
|
-
export const beforeEach = function (callback?: Callback) {
|
|
490
|
-
beforeEachCb = callback;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export const afterEach = function (callback?: Callback) {
|
|
494
|
-
afterEachCb = callback;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
export const it = async function(expectation: string, callback: () => void | Promise<void>, options?: { timeout?: number } | number) {
|
|
499
|
-
const timeoutMs = typeof options === 'number'
|
|
500
|
-
? options
|
|
501
|
-
: (options?.timeout ?? timeoutConfig.testTimeout);
|
|
502
|
-
|
|
503
|
-
const t0 = now();
|
|
504
|
-
try {
|
|
505
|
-
if(typeof beforeEachCb === 'function') {
|
|
506
|
-
await beforeEachCb();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
await withTimeout(callback, timeoutMs, expectation);
|
|
510
|
-
|
|
511
|
-
if(typeof afterEachCb === 'function') {
|
|
512
|
-
await afterEachCb();
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const duration = now() - t0;
|
|
516
|
-
print(` ${GREEN}✔${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
|
|
517
|
-
}
|
|
518
|
-
catch(e) {
|
|
519
|
-
const duration = now() - t0;
|
|
520
|
-
if (!e.__testFailureCounted) {
|
|
521
|
-
++countTestsFailed;
|
|
522
|
-
}
|
|
523
|
-
testErrors.push({ suite: currentSuite, test: expectation, message: e.message ?? String(e) });
|
|
524
|
-
const icon = e instanceof TimeoutError ? '⏱' : '❌';
|
|
525
|
-
print(` ${RED}${icon}${RESET} ${GRAY}${expectation} (${formatDuration(duration)})${RESET}`);
|
|
526
|
-
print(`${RED}${e.message}${RESET}`);
|
|
527
|
-
if (e.stack) print(e.stack);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
it.skip = async function(expectation: string, _callback?: () => void | Promise<void>) {
|
|
532
|
-
++countTestsIgnored;
|
|
533
|
-
print(` ${BLUE}-${RESET} ${GRAY}${expectation} (skipped)${RESET}`);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
export const expect = function(actualValue: any) {
|
|
537
|
-
++countTestsOverall;
|
|
538
|
-
|
|
539
|
-
const expecter = new MatcherFactory(actualValue, true);
|
|
540
|
-
|
|
541
|
-
return expecter;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export const assert = function(success: any, message?: string | Error) {
|
|
545
|
-
++countTestsOverall;
|
|
546
|
-
|
|
547
|
-
if(!success) {
|
|
548
|
-
++countTestsFailed;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
nodeAssert(success, message);
|
|
553
|
-
} catch (error) {
|
|
554
|
-
(error as any).__testFailureCounted = true;
|
|
555
|
-
throw error;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
assert.strictEqual = function<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T {
|
|
560
|
-
++countTestsOverall;
|
|
561
|
-
try {
|
|
562
|
-
nodeAssert.strictEqual(actual, expected, message);
|
|
563
|
-
} catch (error) {
|
|
564
|
-
++countTestsFailed;
|
|
565
|
-
(error as any).__testFailureCounted = true;
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
assert.throws = function(promiseFn: () => unknown, ...args: any[]) {
|
|
571
|
-
++countTestsOverall;
|
|
572
|
-
let error: any;
|
|
573
|
-
try {
|
|
574
|
-
promiseFn();
|
|
575
|
-
} catch (e) {
|
|
576
|
-
error = e;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if(!error) ++countTestsFailed;
|
|
580
|
-
|
|
581
|
-
nodeAssert.throws(() => { if(error) throw error }, args[0], args[1])
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
assert.deepStrictEqual = function<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T {
|
|
585
|
-
++countTestsOverall;
|
|
586
|
-
try {
|
|
587
|
-
nodeAssert.deepStrictEqual(actual, expected, message);
|
|
588
|
-
} catch (error) {
|
|
589
|
-
++countTestsFailed;
|
|
590
|
-
(error as any).__testFailureCounted = true;
|
|
591
|
-
throw error;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// TODO wrap more assert methods
|
|
596
|
-
|
|
597
|
-
const runTests = async function(namespaces: Namespaces) {
|
|
598
|
-
// recursively check the test directory for executable tests
|
|
599
|
-
for( const subNamespace in namespaces ) {
|
|
600
|
-
const namespace = namespaces[subNamespace];
|
|
601
|
-
// execute any test functions
|
|
602
|
-
if(typeof namespace === 'function' ) {
|
|
603
|
-
await namespace();
|
|
604
|
-
}
|
|
605
|
-
// descend into subfolders and objects
|
|
606
|
-
else if( typeof namespace === 'object' ) {
|
|
607
|
-
await runTests(namespace);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const browserSignalDone = () => {
|
|
613
|
-
const doc = (globalThis as any).document;
|
|
614
|
-
if (!doc) return;
|
|
615
|
-
(globalThis as any).__gjsify_test_results = {
|
|
616
|
-
passed: countTestsOverall - countTestsFailed,
|
|
617
|
-
failed: countTestsFailed,
|
|
618
|
-
total: countTestsOverall,
|
|
619
|
-
errors: testErrors,
|
|
620
|
-
};
|
|
621
|
-
doc.documentElement.dataset.testsDone = 'true';
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
const printResult = () => {
|
|
625
|
-
const totalMs = runStartTime > 0 ? now() - runStartTime : 0;
|
|
626
|
-
const durationStr = totalMs > 0 ? ` ${GRAY}(${formatDuration(totalMs)})` : '';
|
|
627
|
-
|
|
628
|
-
if( countTestsIgnored ) {
|
|
629
|
-
// some tests ignored
|
|
630
|
-
print(`\n${BLUE}✔ ${countTestsIgnored} ignored test${ countTestsIgnored > 1 ? 's' : ''}${RESET}`);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if( countTestsFailed ) {
|
|
634
|
-
// some tests failed
|
|
635
|
-
print(`\n${RED}❌ ${countTestsFailed} of ${countTestsOverall} tests failed${durationStr}${RESET}`);
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
// all tests okay
|
|
639
|
-
print(`\n${GREEN}✔ ${countTestsOverall} completed${durationStr}${RESET}`);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const getRuntime = async () => {
|
|
644
|
-
if(runtime && runtime !== 'Unknown') {
|
|
645
|
-
return runtime;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if(globalThis.Deno?.version?.deno) {
|
|
649
|
-
return 'Deno ' + globalThis.Deno?.version?.deno;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Check process (GJS / Node) BEFORE document: @gjsify/dom-elements can set
|
|
653
|
-
// globalThis.document on GJS, which would otherwise cause a false browser-positive.
|
|
654
|
-
// dynamic import('process') throws in the browser so this stays safe there.
|
|
655
|
-
{
|
|
656
|
-
let process = globalThis.process;
|
|
657
|
-
|
|
658
|
-
if(!process) {
|
|
659
|
-
try {
|
|
660
|
-
process = await import('process');
|
|
661
|
-
} catch (_e) {
|
|
662
|
-
// browser or runtime without process — fall through to document check
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if(process?.versions?.gjs) {
|
|
667
|
-
runtime = 'Gjs ' + process.versions.gjs;
|
|
668
|
-
return runtime;
|
|
669
|
-
} else if (process?.versions?.node) {
|
|
670
|
-
runtime = 'Node.js ' + process.versions.node;
|
|
671
|
-
return runtime;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Only treat as Browser after confirming no Node/GJS process is present.
|
|
676
|
-
// dynamic imports throw in browsers, so we are safely past that path here.
|
|
677
|
-
if (typeof (globalThis as any).document !== 'undefined') {
|
|
678
|
-
runtime = 'Browser';
|
|
679
|
-
return runtime;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return runtime || 'Unknown';
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const printRuntime = async () => {
|
|
686
|
-
const runtime = await getRuntime()
|
|
687
|
-
print(`\nRunning on ${runtime}`);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export const run = async (namespaces: Namespaces, options?: { timeout?: number; testTimeout?: number; suiteTimeout?: number } | number) => {
|
|
691
|
-
|
|
692
|
-
applyEnvOverrides();
|
|
693
|
-
runStartTime = now();
|
|
694
|
-
|
|
695
|
-
if (options) {
|
|
696
|
-
if (typeof options === 'number') {
|
|
697
|
-
timeoutConfig.runTimeout = options;
|
|
698
|
-
} else {
|
|
699
|
-
if (options.timeout !== undefined) timeoutConfig.runTimeout = options.timeout;
|
|
700
|
-
if (options.testTimeout !== undefined) timeoutConfig.testTimeout = options.testTimeout;
|
|
701
|
-
if (options.suiteTimeout !== undefined) timeoutConfig.suiteTimeout = options.suiteTimeout;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
printRuntime()
|
|
706
|
-
.then(async () => {
|
|
707
|
-
try {
|
|
708
|
-
await withTimeout(() => runTests(namespaces), timeoutConfig.runTimeout, 'entire test run');
|
|
709
|
-
} catch (e) {
|
|
710
|
-
if (e instanceof TimeoutError) {
|
|
711
|
-
print(`\n${RED}⏱ ${e.message}${RESET}`);
|
|
712
|
-
++countTestsFailed;
|
|
713
|
-
} else {
|
|
714
|
-
throw e;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
})
|
|
718
|
-
.then(async () => {
|
|
719
|
-
printResult();
|
|
720
|
-
browserSignalDone();
|
|
721
|
-
print();
|
|
722
|
-
|
|
723
|
-
quitMainLoop(); // Pre-quit ensureMainLoop's loop so it exits immediately when the hook fires
|
|
724
|
-
mainloop?.quit();
|
|
725
|
-
|
|
726
|
-
// Node.js: exit here (code after mainloop?.run() executes before tests on Node.js)
|
|
727
|
-
if (!mainloop) {
|
|
728
|
-
const exitCode = countTestsFailed > 0 ? 1 : 0;
|
|
729
|
-
try {
|
|
730
|
-
const process = globalThis.process || await import('process');
|
|
731
|
-
process.exit(exitCode);
|
|
732
|
-
} catch (_e) { /* process unavailable */ }
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
// Run the GJS mainloop for async operations (blocks until mainloop.quit() is called)
|
|
737
|
-
mainloop?.run();
|
|
738
|
-
|
|
739
|
-
// GJS: exit after mainloop returns (system.exit() inside a mainloop
|
|
740
|
-
// callback does not terminate immediately)
|
|
741
|
-
if (mainloop) {
|
|
742
|
-
const exitCode = countTestsFailed > 0 ? 1 : 0;
|
|
743
|
-
try {
|
|
744
|
-
(globalThis as any).imports.system.exit(exitCode);
|
|
745
|
-
} catch (_e) { /* system.exit unavailable */ }
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
export default {
|
|
750
|
-
run,
|
|
751
|
-
assert,
|
|
752
|
-
expect,
|
|
753
|
-
it,
|
|
754
|
-
afterEach,
|
|
755
|
-
beforeEach,
|
|
756
|
-
on,
|
|
757
|
-
describe,
|
|
758
|
-
configure,
|
|
759
|
-
print,
|
|
760
|
-
}
|