@gjsify/unit 0.0.2

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.
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect, assert, beforeEach, afterEach } from '@gjsify/unit';
2
+
3
+ export default async () => {
4
+
5
+ await describe('assert', async () => {
6
+
7
+ await it('should consider truthy values as valid', async () => {
8
+ assert(true);
9
+ assert(1);
10
+ assert({});
11
+ assert([]);
12
+ });
13
+ })
14
+
15
+ await describe('beforeEach', async () => {
16
+
17
+ let foo = '';
18
+ let count = 0;
19
+ let countAfter = 0;
20
+
21
+ beforeEach(async () => {
22
+ console.log("beforeEach");
23
+ foo = 'bar';
24
+ ++count;
25
+ });
26
+
27
+ afterEach(async () => {
28
+ console.log("afterEach");
29
+ --countAfter;
30
+ });
31
+
32
+ await it('foo should be "bar"', async () => {
33
+ expect(foo).toBe('bar');
34
+ foo = 'override me again'
35
+ });
36
+
37
+ await it('foo should be "bar" again', async () => {
38
+ expect(foo).toBe('bar');
39
+ });
40
+
41
+ await it('count should be 3 again', async () => {
42
+ expect(count).toBe(3);
43
+ });
44
+
45
+ await it('countAfter should be -3', async () => {
46
+ expect(countAfter).toBe(-3);
47
+ });
48
+ });
49
+
50
+ await describe('expect::to', async () => {
51
+ await it('should be possible to validate expectations by callback', async () => {
52
+ expect(3).to(function(actualValue) {
53
+ return actualValue === 3;
54
+ });
55
+ });
56
+
57
+ await it('should be possible to invalidate expectations by callback', async () => {
58
+ expect(3).not.to(function(actualValue) {
59
+ return actualValue === 5;
60
+ });
61
+ });
62
+ });
63
+
64
+ await describe('expect::toBe', async () => {
65
+ var obj = {};
66
+ var obj2 = {};
67
+
68
+ await it('should compare using ===', async () => {
69
+ expect(true).toBe(true);
70
+ expect(false).toBe(false);
71
+ expect('test').toBe('test');
72
+ expect(obj).toBe(obj);
73
+ });
74
+
75
+ await it('should compare using !==', async () => {
76
+ expect(true).not.toBe(false);
77
+ expect(true).not.toBe(1);
78
+ expect(false).not.toBe(true);
79
+ expect(false).not.toBe(0);
80
+ expect('test').not.toBe('test2');
81
+ expect(obj).not.toBe(obj2);
82
+ });
83
+ });
84
+
85
+ await describe('expect::toEqual', async () => {
86
+ var obj = {};
87
+ var obj2 = {};
88
+
89
+ await it('should compare using ==', async () => {
90
+ expect(true).toEqual(true);
91
+ expect(true).toEqual(1);
92
+ expect(false).toEqual(false);
93
+ expect(false).toEqual(0);
94
+ expect('test').toEqual('test');
95
+ expect(obj).toEqual(obj);
96
+ });
97
+
98
+ await it('should compare using !=', async () => {
99
+ expect(true).not.toEqual(false);
100
+ expect(false).not.toEqual(true);
101
+ expect('test').not.toEqual('test2');
102
+ expect(obj).not.toEqual(obj2);
103
+ });
104
+ });
105
+
106
+ await describe('expect::toMatch', async () => {
107
+ await it('should consider matching regular expressions as valid', async () => {
108
+ expect('test').toMatch(/test/);
109
+ expect('test').toMatch(/est/);
110
+ expect('test').toMatch('test');
111
+ });
112
+
113
+ await it('should consider non matching regular expressions as invalid', async () => {
114
+ expect('test').not.toMatch(/tester/);
115
+ });
116
+ });
117
+
118
+ await describe('expect::toBeDefined', async () => {
119
+ var obj = {key: 'value'};
120
+
121
+ await it('should consider defined values as valid', async () => {
122
+ expect(obj.key).toBeDefined();
123
+ });
124
+
125
+ await it('should consider undefined values as invalid', async () => {
126
+ expect((obj as any).invalidKey).not.toBeDefined();
127
+ });
128
+ });
129
+
130
+ await describe('expect::toBeUndefined', async () => {
131
+ var obj = {key: 'value'};
132
+
133
+ await it('should consider undefined values as valid', async () => {
134
+ expect((obj as any).invalidKey).toBeUndefined();
135
+ });
136
+
137
+ await it('should consider defined values as invalid', async () => {
138
+ expect(obj.key).not.toBeUndefined();
139
+ });
140
+ });
141
+
142
+ await describe('expect::toBeNull', async () => {
143
+ await it('should consider null values as valid', async () => {
144
+ expect(null).toBeNull();
145
+ });
146
+
147
+ await it('should consider non null values as invalid', async () => {
148
+ expect(0).not.toBeNull();
149
+ expect(false).not.toBeNull();
150
+ expect('').not.toBeNull();
151
+ expect('null').not.toBeNull();
152
+ expect(undefined).not.toBeNull();
153
+ expect({}).not.toBeNull();
154
+ });
155
+ });
156
+
157
+ await describe('expect::toBeTruthy', async () => {
158
+ await it('should consider truthy values as valid', async () => {
159
+ expect(true).toBeTruthy();
160
+ expect(1).toBeTruthy();
161
+ expect({}).toBeTruthy();
162
+ expect([]).toBeTruthy();
163
+ });
164
+
165
+ await it('should consider non truthy values as invalid', async () => {
166
+ expect(false).not.toBeTruthy();
167
+ expect(0).not.toBeTruthy();
168
+ expect('').not.toBeTruthy();
169
+ expect(null).not.toBeTruthy();
170
+ expect(undefined).not.toBeTruthy();
171
+ });
172
+ });
173
+
174
+ await describe('expect::toBeFalsy', async () => {
175
+ await it('should consider truthy values as valid', async () => {
176
+ expect(false).toBeFalsy();
177
+ expect(0).toBeFalsy();
178
+ expect('').toBeFalsy();
179
+ expect(null).toBeFalsy();
180
+ expect(undefined).toBeFalsy();
181
+ });
182
+
183
+ await it('should consider non truthy values as invalid', async () => {
184
+ expect(true).not.toBeFalsy();
185
+ expect(1).not.toBeFalsy();
186
+ expect({}).not.toBeFalsy();
187
+ expect([]).not.toBeFalsy();
188
+ });
189
+ });
190
+
191
+ await describe('expect::toContain', async () => {
192
+ var testArray = [1, 'a'];
193
+
194
+ await it('should consider array containing a value as valid', async () => {
195
+ expect(testArray).toContain(1);
196
+ expect(testArray).toContain('a');
197
+ });
198
+
199
+ await it('should consider arrays not containing a value as invalid', async () => {
200
+ expect(testArray).not.toContain(0);
201
+ expect(testArray).not.toContain('b');
202
+ });
203
+ });
204
+
205
+ await describe('expect::toBeLessThan', async () => {
206
+ await it('should consider greater values as valid', async () => {
207
+ expect(1).toBeLessThan(2);
208
+ expect(1).toBeLessThan(200);
209
+ });
210
+
211
+ await it('should consider equal values as invalid', async () => {
212
+ expect(1).not.toBeLessThan(1);
213
+ });
214
+
215
+ await it('should consider smaller values as invalid', async () => {
216
+ expect(1).not.toBeLessThan(0);
217
+ expect(1).not.toBeLessThan(-5);
218
+ });
219
+ });
220
+
221
+ await describe('expect::toBeGreaterThan', async () => {
222
+ await it('should consider smaller values as valid', async () => {
223
+ expect(2).toBeGreaterThan(1);
224
+ expect(2).toBeGreaterThan(0);
225
+ expect(2).toBeGreaterThan(-5);
226
+ });
227
+
228
+ await it('should consider equal values as invalid', async () => {
229
+ expect(1).not.toBeGreaterThan(1);
230
+ });
231
+
232
+ await it('should consider greater values as invalid', async () => {
233
+ expect(1).not.toBeGreaterThan(2);
234
+ expect(1).not.toBeGreaterThan(200);
235
+ });
236
+ });
237
+
238
+ await describe('expect::toBeCloseTo', async () => {
239
+ var pi = 3.1415926, e = 2.78;
240
+
241
+ await it('should consider close numbers as valid', async () => {
242
+ expect(pi).toBeCloseTo(e, 0);
243
+ });
244
+
245
+ await it('should consider non close numbers as invalid', async () => {
246
+ expect(pi).not.toBeCloseTo(e, 2);
247
+ });
248
+ });
249
+
250
+ await describe('expect::toBeCloseTo', async () => {
251
+ function throwException() { throw {}; }
252
+ function dontThrowException() {}
253
+
254
+ await it('should consider functions throwing an exception as valid', async () => {
255
+ expect(throwException).toThrow();
256
+ });
257
+
258
+ await it('should consider functions not throwing an exception as invalid', async () => {
259
+ expect(dontThrowException).not.toThrow();
260
+ });
261
+ });
262
+ }
package/src/index.ts ADDED
@@ -0,0 +1,425 @@
1
+ // Based on https://github.com/philipphoffmann/gjsunit
2
+
3
+ import "@girs/gjs";
4
+
5
+ import type GLib from 'gi://GLib?version=2.0';
6
+ export * from './spy.js';
7
+ import nodeAssert from 'assert';
8
+
9
+ const mainloop: GLib.MainLoop | undefined = (globalThis as any)?.imports?.mainloop;
10
+
11
+ let countTestsOverall = 0;
12
+ let countTestsFailed = 0;
13
+ let countTestsIgnored = 0;
14
+ let runtime = '';
15
+
16
+ const RED = '\x1B[31m';
17
+ const GREEN = '\x1B[32m';
18
+ const BLUE = '\x1b[34m';
19
+ const GRAY = '\x1B[90m';
20
+ const RESET = '\x1B[39m';
21
+
22
+ export interface Namespaces {
23
+ [key: string]: () => (Promise<void>) | Namespaces;
24
+ }
25
+
26
+ export type Callback = () => Promise<void>;
27
+
28
+ export type Runtime = 'Gjs' | 'Deno' | 'Node.js' | 'Unknown' | 'Browser';
29
+
30
+ // Makes this work on Gjs and Node.js
31
+ export const print = globalThis.print || console.log;
32
+
33
+ class MatcherFactory {
34
+
35
+ public not: MatcherFactory;
36
+
37
+ constructor(protected readonly actualValue: any, protected readonly positive: boolean, negated?: MatcherFactory) {
38
+ if(negated) {
39
+ this.not = negated;
40
+ } else {
41
+ this.not = new MatcherFactory(actualValue, !positive, this);
42
+ }
43
+ }
44
+
45
+ triggerResult(success: boolean, msg: string) {
46
+ if( (success && !this.positive) ||
47
+ (!success && this.positive) ) {
48
+ ++countTestsFailed;
49
+ throw new Error(msg);
50
+ }
51
+ }
52
+
53
+ to(callback: (actualValue: any) => boolean) {
54
+ this.triggerResult(callback(this.actualValue),
55
+ ` Expected callback to validate`
56
+ );
57
+ }
58
+
59
+ toBe(expectedValue: any) {
60
+ this.triggerResult(this.actualValue === expectedValue,
61
+ ` Expected values to match using ===\n` +
62
+ ` Expected: ${expectedValue} (${typeof expectedValue})\n` +
63
+ ` Actual: ${this.actualValue} (${typeof this.actualValue})`
64
+ );
65
+ }
66
+
67
+ toEqual(expectedValue: any) {
68
+ this.triggerResult(this.actualValue == expectedValue,
69
+ ` Expected values to match using ==\n` +
70
+ ` Expected: ${expectedValue} (${typeof expectedValue})\n` +
71
+ ` Actual: ${this.actualValue} (${typeof this.actualValue})`
72
+ );
73
+ }
74
+
75
+ toEqualArray(expectedValue: Array<any> | Uint8Array) {
76
+
77
+ let success = Array.isArray(this.actualValue) && Array.isArray(expectedValue) && this.actualValue.length === expectedValue.length;
78
+
79
+ for (let i = 0; i < this.actualValue.length; i++) {
80
+ const actualVal = this.actualValue[i];
81
+ const expectedVal = expectedValue[i];
82
+ success = actualVal == expectedVal;
83
+ if(!success) break;
84
+ }
85
+
86
+ this.triggerResult(success,
87
+ ` Expected array items to match using ==\n` +
88
+ ` Expected: ${expectedValue} (${typeof expectedValue})\n` +
89
+ ` Actual: ${this.actualValue} (${typeof this.actualValue})`
90
+ );
91
+ }
92
+
93
+ toMatch(expectedValue: any) {
94
+ if(typeof this.actualValue.match !== 'function') {
95
+ throw new Error(`You can not use toMatch on type ${typeof this.actualValue}`);
96
+ }
97
+ this.triggerResult(!!this.actualValue.match(expectedValue),
98
+ ' Expected values to match using regular expression\n' +
99
+ ' Expression: ' + expectedValue + '\n' +
100
+ ' Actual: ' + this.actualValue
101
+ );
102
+ }
103
+
104
+ toBeDefined() {
105
+ this.triggerResult(typeof this.actualValue !== 'undefined',
106
+ ` Expected value to be defined`
107
+ );
108
+ }
109
+
110
+ toBeUndefined() {
111
+ this.triggerResult(typeof this.actualValue === 'undefined',
112
+ ` Expected value to be undefined`
113
+ );
114
+ }
115
+
116
+ toBeNull() {
117
+ this.triggerResult(this.actualValue === null,
118
+ ` Expected value to be null`
119
+ );
120
+ }
121
+
122
+ toBeTruthy() {
123
+ this.triggerResult(this.actualValue as unknown as boolean,
124
+ ` Expected value to be truthy`
125
+ );
126
+ }
127
+
128
+ toBeFalsy() {
129
+ this.triggerResult(!this.actualValue,
130
+ ` Expected value to be falsy`
131
+ );
132
+ }
133
+
134
+ toContain(needle: any) {
135
+ this.triggerResult(this.actualValue instanceof Array && this.actualValue.indexOf(needle) !== -1,
136
+ ` Expected ` + this.actualValue + ` to contain ` + needle
137
+ );
138
+ }
139
+ toBeLessThan(greaterValue: number) {
140
+ this.triggerResult(this.actualValue < greaterValue,
141
+ ` Expected ` + this.actualValue + ` to be less than ` + greaterValue
142
+ );
143
+ }
144
+ toBeGreaterThan(smallerValue: number) {
145
+ this.triggerResult(this.actualValue > smallerValue,
146
+ ` Expected ` + this.actualValue + ` to be greater than ` + smallerValue
147
+ );
148
+ }
149
+ toBeCloseTo(expectedValue: number, precision: number) {
150
+ const shiftHelper = Math.pow(10, precision);
151
+ this.triggerResult(Math.round((this.actualValue as unknown as number) * shiftHelper) / shiftHelper === Math.round(expectedValue * shiftHelper) / shiftHelper,
152
+ ` Expected ` + this.actualValue + ` with precision ` + precision + ` to be close to ` + expectedValue
153
+ );
154
+ }
155
+ toThrow(ErrorType?: typeof Error) {
156
+ let errorMessage = '';
157
+ let didThrow = false;
158
+ let typeMatch = true;
159
+ try {
160
+ this.actualValue();
161
+ didThrow = false;
162
+ }
163
+ catch(e) {
164
+ errorMessage = e.message || '';
165
+ didThrow = true;
166
+ if(ErrorType) {
167
+ typeMatch = (e instanceof ErrorType)
168
+ }
169
+ }
170
+ const functionName = this.actualValue.name || typeof this.actualValue === 'function' ? "[anonymous function]" : this.actualValue.toString();
171
+ this.triggerResult(didThrow,
172
+ ` Expected ${functionName} to ${this.positive ? 'throw' : 'not throw'} an exception ${!this.positive && errorMessage ? `, but an error with the message "${errorMessage}" was thrown` : ''}`
173
+ );
174
+
175
+ if(ErrorType) {
176
+ this.triggerResult(typeMatch,
177
+ ` Expected Error type '${ErrorType.name}', but the error is not an instance of it`
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ export const describe = async function(moduleName: string, callback: Callback) {
184
+ print('\n' + moduleName);
185
+
186
+ await callback();
187
+
188
+ // Reset after and before callbacks
189
+ beforeEachCb = null;
190
+ afterEachCb = null;
191
+ };
192
+
193
+ const runtimeMatch = async function(onRuntime: Runtime[], version?: string) {
194
+
195
+ const currRuntime = (await getRuntime());
196
+
197
+ const foundRuntime = onRuntime.find((r) => currRuntime.includes(r));
198
+
199
+ if (!foundRuntime) {
200
+ return {
201
+ matched: false
202
+ }
203
+ }
204
+
205
+ if(typeof version === 'string') {
206
+ // TODO allow version wildcards like 16.x.x
207
+ if(!currRuntime.includes(version)) {
208
+ return {
209
+ matched: false
210
+ }
211
+ }
212
+ }
213
+
214
+ return {
215
+ matched: true,
216
+ runtime: foundRuntime,
217
+ version: version,
218
+ }
219
+ }
220
+
221
+ // TODO add support for Browser
222
+ /** E.g on('Deno', () { it(...) }) */
223
+ export const on = async function(onRuntime: Runtime | Runtime[], version: string | Callback, callback?: Callback) {
224
+
225
+ if(typeof onRuntime === 'string') {
226
+ onRuntime = [onRuntime];
227
+ }
228
+
229
+ if(typeof version === 'function') {
230
+ callback = version;
231
+ version = undefined;
232
+ }
233
+
234
+ const { matched } = await runtimeMatch(onRuntime, version as string | undefined);
235
+
236
+ if(!matched) {
237
+ ++countTestsIgnored;
238
+ return;
239
+ }
240
+
241
+ print(`\nOn ${onRuntime.join(', ')}${version ? ' ' + version : ''}`);
242
+
243
+ await callback();
244
+ }
245
+
246
+ let beforeEachCb: Callback | undefined | null;
247
+ let afterEachCb: Callback | undefined | null;
248
+
249
+ export const beforeEach = function (callback?: Callback) {
250
+ beforeEachCb = callback;
251
+ }
252
+
253
+ export const afterEach = function (callback?: Callback) {
254
+ afterEachCb = callback;
255
+ }
256
+
257
+
258
+ export const it = async function(expectation: string, callback: () => void | Promise<void>) {
259
+ try {
260
+ if(typeof beforeEachCb === 'function') {
261
+ await beforeEachCb();
262
+ }
263
+
264
+ await callback();
265
+
266
+ if(typeof afterEachCb === 'function') {
267
+ await afterEachCb();
268
+ }
269
+
270
+ print(` ${GREEN}✔${RESET} ${GRAY}${expectation}${RESET}`);
271
+ }
272
+ catch(e) {
273
+ print(` ${RED}❌${RESET} ${GRAY}${expectation}${RESET}`);
274
+ print(`${RED}${e.message}${RESET}`);
275
+ if (e.stack) print(e.stack);
276
+ }
277
+ }
278
+
279
+ export const expect = function(actualValue: any) {
280
+ ++countTestsOverall;
281
+
282
+ const expecter = new MatcherFactory(actualValue, true);
283
+
284
+ return expecter;
285
+ }
286
+
287
+ export const assert = function(success: any, message?: string | Error) {
288
+ ++countTestsOverall;
289
+
290
+ if(!success) {
291
+ ++countTestsFailed;
292
+ }
293
+
294
+ nodeAssert(success, message);
295
+ }
296
+
297
+ assert.strictEqual = function<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T {
298
+ ++countTestsOverall;
299
+ try {
300
+ nodeAssert.strictEqual(actual, expected, message);
301
+ } catch (error) {
302
+ ++countTestsFailed;
303
+ throw error
304
+ }
305
+ }
306
+
307
+ assert.throws = function(promiseFn: () => unknown, ...args: any[]) {
308
+ ++countTestsOverall;
309
+ let error: any;
310
+ try {
311
+ promiseFn();
312
+ } catch (e) {
313
+ error = e;
314
+ }
315
+
316
+ if(!error) ++countTestsFailed;
317
+
318
+ nodeAssert.throws(() => { if(error) throw error }, args[0], args[1])
319
+ };
320
+
321
+ assert.deepStrictEqual = function<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T {
322
+ ++countTestsOverall;
323
+ try {
324
+ nodeAssert.deepStrictEqual(actual, expected, message);
325
+ } catch (error) {
326
+ ++countTestsFailed;
327
+ throw error
328
+ }
329
+ }
330
+
331
+ // TODO wrap more assert methods
332
+
333
+ const runTests = async function(namespaces: Namespaces) {
334
+ // recursively check the test directory for executable tests
335
+ for( const subNamespace in namespaces ) {
336
+ const namespace = namespaces[subNamespace];
337
+ // execute any test functions
338
+ if(typeof namespace === 'function' ) {
339
+ await namespace();
340
+ }
341
+ // descend into subfolders and objects
342
+ else if( typeof namespace === 'object' ) {
343
+ await runTests(namespace);
344
+ }
345
+ }
346
+ }
347
+
348
+ const printResult = () => {
349
+
350
+ if( countTestsIgnored ) {
351
+ // some tests ignored
352
+ print(`\n${BLUE}✔ ${countTestsIgnored} ignored test${ countTestsIgnored > 1 ? 's' : ''}${RESET}`);
353
+ }
354
+
355
+ if( countTestsFailed ) {
356
+ // some tests failed
357
+ print(`\n${RED}❌ ${countTestsFailed} of ${countTestsOverall} tests failed${RESET}`);
358
+ }
359
+ else {
360
+ // all tests okay
361
+ print(`\n${GREEN}✔ ${countTestsOverall} completed${RESET}`);
362
+ }
363
+ }
364
+
365
+ const getRuntime = async () => {
366
+ if(runtime && runtime !== 'Unknown') {
367
+ return runtime;
368
+ }
369
+
370
+ if(globalThis.Deno?.version?.deno) {
371
+ return 'Deno ' + globalThis.Deno?.version?.deno;
372
+ } else {
373
+ let process = globalThis.process;
374
+
375
+ if(!process) {
376
+ try {
377
+ process = await import('process');
378
+ } catch (error) {
379
+ console.error(error)
380
+ console.warn(error.message);
381
+ runtime = 'Unknown'
382
+ }
383
+ }
384
+
385
+ if(process?.versions?.gjs) {
386
+ runtime = 'Gjs ' + process.versions.gjs;
387
+ } else if (process?.versions?.node) {
388
+ runtime = 'Node.js ' + process.versions.node;
389
+ }
390
+ }
391
+ return runtime || 'Unknown';
392
+ }
393
+
394
+ const printRuntime = async () => {
395
+ const runtime = await getRuntime()
396
+ print(`\nRunning on ${runtime}`);
397
+ }
398
+
399
+ export const run = async (namespaces: Namespaces) => {
400
+
401
+ printRuntime()
402
+ .then(async () => {
403
+ return runTests(namespaces)
404
+ .then(() => {
405
+ printResult();
406
+ print();
407
+ mainloop?.quit();
408
+ })
409
+ });
410
+
411
+ // Run the GJS mainloop for async operations
412
+ mainloop?.run();
413
+ }
414
+
415
+ export default {
416
+ run,
417
+ assert,
418
+ expect,
419
+ it,
420
+ afterEach,
421
+ beforeEach,
422
+ on,
423
+ describe,
424
+ print,
425
+ }