as-test 1.2.0 → 1.4.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.
@@ -1,15 +1,39 @@
1
1
  import { visualize } from "../util/helpers";
2
2
  import { Tests } from "./tests";
3
- import { JSON } from "json-as/assembly";
4
3
  import { namedSnapshotKey, nextUnnamedSnapshotKey } from "..";
5
4
  import {
6
5
  sendAssertionFailure,
7
6
  sendWarning,
8
7
  snapshotAssert,
9
8
  } from "../util/wipc";
10
- import { OBJECT, TOTAL_OVERHEAD } from "~lib/rt/common";
9
+ import { reflectEquals } from "./reflect";
10
+ import { stringify, escape } from "./stringify";
11
11
 
12
12
  let warnedToThrowDisabled = false;
13
+ let warnedSafeStringifyMissing = false;
14
+
15
+ function safeStringify<T>(value: T): string {
16
+ if (
17
+ isManaged<T>() &&
18
+ !warnedSafeStringifyMissing &&
19
+ changetype<usize>(value) != 0 &&
20
+ !isString<T>() &&
21
+ !isArray<T>()
22
+ ) {
23
+ // @ts-expect-error: optional user-supplied serializer
24
+ if (!isDefined(value.toJSON)) {
25
+ sendWarning(
26
+ "Class " +
27
+ nameof<T>() +
28
+ " has no toJSON(): string method. Report values render as a `<" +
29
+ nameof<T>() +
30
+ ">` placeholder. Add toJSON() returning a JSON string to serialize.",
31
+ );
32
+ warnedSafeStringifyMissing = true;
33
+ }
34
+ }
35
+ return stringify<T>(value);
36
+ }
13
37
 
14
38
  export class Expectation<T> extends Tests {
15
39
  public verdict: string = "none";
@@ -18,13 +42,11 @@ export class Expectation<T> extends Tests {
18
42
 
19
43
  private _left: T;
20
44
 
21
- // @ts-ignore
45
+ // @ts-expect-error: used internally
22
46
  private _right: u64 = 0;
23
47
 
24
- // @ts-ignore
25
48
  private _not: boolean = false;
26
49
 
27
- // @ts-ignore
28
50
  private _skip: boolean = false;
29
51
 
30
52
  private _message: string = "";
@@ -57,6 +79,41 @@ export class Expectation<T> extends Tests {
57
79
  return this;
58
80
  }
59
81
 
82
+ /**
83
+ * Asserts that a custom predicate holds. Accepts either a bool or a
84
+ * `() => bool` lambda. Useful when the verdict isn't expressible via the
85
+ * built-in matchers — e.g. delegating to a hand-written comparator.
86
+ *
87
+ * expect(x).where(x > 0 && x < 10);
88
+ * expect(actual).where((): bool => deepEqual(GLOBAL_A, GLOBAL_B));
89
+ *
90
+ * Chains with other matchers as an independent assertion:
91
+ *
92
+ * expect(7).toBe(7).where((): bool => isFresh());
93
+ *
94
+ * Note: AssemblyScript does not implement closures, so the lambda cannot
95
+ * capture local variables from the enclosing scope. Use the bool form when
96
+ * the predicate references locals, or refer to module-level values from
97
+ * inside the lambda.
98
+ */
99
+ where<W>(predicate: W, message: string = ""): Expectation<T> {
100
+ let passed: bool;
101
+ if (isFunction<W>()) {
102
+ passed = (predicate as () => bool)();
103
+ } else {
104
+ // @ts-ignore: W is a bool-compatible primitive in this branch
105
+ passed = predicate as bool;
106
+ }
107
+ this._resolve(
108
+ passed,
109
+ "where",
110
+ q(passed ? "true" : "false"),
111
+ q("true"),
112
+ message,
113
+ );
114
+ return this;
115
+ }
116
+
60
117
  private _resolve(
61
118
  passed: bool,
62
119
  instr: string,
@@ -74,14 +131,26 @@ export class Expectation<T> extends Tests {
74
131
  return;
75
132
  }
76
133
  const isFail = this._not ? passed : !passed;
77
- this.verdict = isFail ? "fail" : "ok";
78
- this.instr = instr;
79
- this.left = left;
80
- this.right = right;
81
134
  const resolvedMessage = message.length ? message : this._message;
82
- this.message = isFail ? resolvedMessage : "";
135
+ // When matchers chain, later ones must not overwrite an earlier failure's
136
+ // recorded state — otherwise a passing matcher after a failed one would
137
+ // flip the suite's verdict back to "ok". Each matcher still fires its own
138
+ // IPC failure independently below.
139
+ if (this.verdict != "fail") {
140
+ this.verdict = isFail ? "fail" : "ok";
141
+ this.instr = instr;
142
+ this.left = left;
143
+ this.right = right;
144
+ this.message = isFail ? resolvedMessage : "";
145
+ }
83
146
  if (isFail) {
84
- sendAssertionFailure(this._snapshotKey, instr, left, right, this.message);
147
+ sendAssertionFailure(
148
+ this._snapshotKey,
149
+ instr,
150
+ left,
151
+ right,
152
+ resolvedMessage,
153
+ );
85
154
  // @ts-ignore
86
155
  if (isDefined(AS_TEST_FUZZ)) {
87
156
  // @ts-ignore
@@ -95,7 +164,7 @@ export class Expectation<T> extends Tests {
95
164
  // @ts-ignore
96
165
  __as_test_fuzz_failure_right = right;
97
166
  // @ts-ignore
98
- __as_test_fuzz_failure_message = this.message;
167
+ __as_test_fuzz_failure_message = resolvedMessage;
99
168
  }
100
169
  }
101
170
  }
@@ -105,7 +174,7 @@ export class Expectation<T> extends Tests {
105
174
  /**
106
175
  * Tests if a == null
107
176
  */
108
- toBeNull(message: string = ""): void {
177
+ toBeNull(message: string = ""): Expectation<T> {
109
178
  const passed =
110
179
  (isNullable<T>() && changetype<usize>(this._left) == 0) ||
111
180
  (isInteger<T>() && nameof<T>() == "usize" && this._left == 0);
@@ -121,12 +190,13 @@ export class Expectation<T> extends Tests {
121
190
  ),
122
191
  message,
123
192
  );
193
+ return this;
124
194
  }
125
195
 
126
196
  /**
127
197
  * Tests if a > b
128
198
  */
129
- toBeGreaterThan(value: T, message: string = ""): void {
199
+ toBeGreaterThan(value: T, message: string = ""): Expectation<T> {
130
200
  if (!isInteger<T>() && !isFloat<T>())
131
201
  ERROR("toBeGreaterThan() can only be used on number types!");
132
202
 
@@ -144,12 +214,13 @@ export class Expectation<T> extends Tests {
144
214
  ),
145
215
  message,
146
216
  );
217
+ return this;
147
218
  }
148
219
 
149
220
  /**
150
221
  * Tests if a >= b
151
222
  */
152
- toBeGreaterOrEqualTo(value: T, message: string = ""): void {
223
+ toBeGreaterOrEqualTo(value: T, message: string = ""): Expectation<T> {
153
224
  if (!isInteger<T>() && !isFloat<T>())
154
225
  ERROR("toBeGreaterOrEqualTo() can only be used on number types!");
155
226
 
@@ -167,12 +238,13 @@ export class Expectation<T> extends Tests {
167
238
  ),
168
239
  message,
169
240
  );
241
+ return this;
170
242
  }
171
243
 
172
244
  /**
173
245
  * Tests if a < b
174
246
  */
175
- toBeLessThan(value: T, message: string = ""): void {
247
+ toBeLessThan(value: T, message: string = ""): Expectation<T> {
176
248
  if (!isInteger<T>() && !isFloat<T>())
177
249
  ERROR("toBeLessThan() can only be used on number types!");
178
250
 
@@ -190,12 +262,13 @@ export class Expectation<T> extends Tests {
190
262
  ),
191
263
  message,
192
264
  );
265
+ return this;
193
266
  }
194
267
 
195
268
  /**
196
269
  * Tests if a <= b
197
270
  */
198
- toBeLessThanOrEqualTo(value: T, message: string = ""): void {
271
+ toBeLessThanOrEqualTo(value: T, message: string = ""): Expectation<T> {
199
272
  if (!isInteger<T>() && !isFloat<T>())
200
273
  ERROR("toBeLessThanOrEqualTo() can only be used on number types!");
201
274
 
@@ -213,12 +286,13 @@ export class Expectation<T> extends Tests {
213
286
  ),
214
287
  message,
215
288
  );
289
+ return this;
216
290
  }
217
291
 
218
292
  /**
219
293
  * Tests if a is string
220
294
  */
221
- toBeString(message: string = ""): void {
295
+ toBeString(message: string = ""): Expectation<T> {
222
296
  this._resolve(
223
297
  isString<T>(),
224
298
  "toBeString",
@@ -226,12 +300,13 @@ export class Expectation<T> extends Tests {
226
300
  q("string"),
227
301
  message,
228
302
  );
303
+ return this;
229
304
  }
230
305
 
231
306
  /**
232
307
  * Tests if a is boolean
233
308
  */
234
- toBeBoolean(message: string = ""): void {
309
+ toBeBoolean(message: string = ""): Expectation<T> {
235
310
  this._resolve(
236
311
  isBoolean<T>(),
237
312
  "toBeBoolean",
@@ -239,12 +314,13 @@ export class Expectation<T> extends Tests {
239
314
  q("boolean"),
240
315
  message,
241
316
  );
317
+ return this;
242
318
  }
243
319
 
244
320
  /**
245
321
  * Tests if a is array
246
322
  */
247
- toBeArray(message: string = ""): void {
323
+ toBeArray(message: string = ""): Expectation<T> {
248
324
  this._resolve(
249
325
  isArray<T>(),
250
326
  "toBeArray",
@@ -252,12 +328,13 @@ export class Expectation<T> extends Tests {
252
328
  q("Array<any>"),
253
329
  message,
254
330
  );
331
+ return this;
255
332
  }
256
333
 
257
334
  /**
258
335
  * Tests if a is number
259
336
  */
260
- toBeNumber(message: string = ""): void {
337
+ toBeNumber(message: string = ""): Expectation<T> {
261
338
  this._resolve(
262
339
  isFloat<T>() || isInteger<T>(),
263
340
  "toBeNumber",
@@ -265,12 +342,13 @@ export class Expectation<T> extends Tests {
265
342
  q("number"),
266
343
  message,
267
344
  );
345
+ return this;
268
346
  }
269
347
 
270
348
  /**
271
349
  * Tests if a is integer
272
350
  */
273
- toBeInteger(message: string = ""): void {
351
+ toBeInteger(message: string = ""): Expectation<T> {
274
352
  this._resolve(
275
353
  isInteger<T>(),
276
354
  "toBeInteger",
@@ -278,12 +356,13 @@ export class Expectation<T> extends Tests {
278
356
  q("integer"),
279
357
  message,
280
358
  );
359
+ return this;
281
360
  }
282
361
 
283
362
  /**
284
363
  * Tests if a is float
285
364
  */
286
- toBeFloat(message: string = ""): void {
365
+ toBeFloat(message: string = ""): Expectation<T> {
287
366
  this._resolve(
288
367
  isFloat<T>(),
289
368
  "toBeFloat",
@@ -291,21 +370,23 @@ export class Expectation<T> extends Tests {
291
370
  q("float"),
292
371
  message,
293
372
  );
373
+ return this;
294
374
  }
295
375
 
296
376
  /**
297
377
  * Tests if a is finite
298
378
  */
299
- toBeFinite(message: string = ""): void {
379
+ toBeFinite(message: string = ""): Expectation<T> {
300
380
  // @ts-ignore
301
381
  const passed = (isFloat<T>() || isInteger<T>()) && isFinite(this._left);
302
382
  this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"), message);
383
+ return this;
303
384
  }
304
385
 
305
386
  /**
306
387
  * Tests if a value is truthy
307
388
  */
308
- toBeTruthy(message: string = ""): void {
389
+ toBeTruthy(message: string = ""): Expectation<T> {
309
390
  this._resolve(
310
391
  isTruthy<T>(this._left),
311
392
  "toBeTruthy",
@@ -313,12 +394,13 @@ export class Expectation<T> extends Tests {
313
394
  q("truthy"),
314
395
  message,
315
396
  );
397
+ return this;
316
398
  }
317
399
 
318
400
  /**
319
401
  * Tests if a value is falsy
320
402
  */
321
- toBeFalsy(message: string = ""): void {
403
+ toBeFalsy(message: string = ""): Expectation<T> {
322
404
  this._resolve(
323
405
  !isTruthy<T>(this._left),
324
406
  "toBeFalsy",
@@ -326,12 +408,14 @@ export class Expectation<T> extends Tests {
326
408
  q("falsy"),
327
409
  message,
328
410
  );
411
+ return this;
329
412
  }
330
413
 
331
414
  /**
332
415
  * Tests if a floating-point number is close to expected
333
416
  */
334
- toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): void {
417
+ // prettier-ignore
418
+ toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): Expectation<T> {
335
419
  if (!isFloat<T>() && !isInteger<T>())
336
420
  ERROR("toBeCloseTo() can only be used on number types!");
337
421
  const factor = Math.pow(10, precision as f64);
@@ -344,12 +428,13 @@ export class Expectation<T> extends Tests {
344
428
  visualize<T>(expected),
345
429
  message,
346
430
  );
431
+ return this;
347
432
  }
348
433
 
349
434
  /**
350
435
  * Tests if a string contains substring
351
436
  */
352
- toMatch(value: string, message: string = ""): void {
437
+ toMatch(value: string, message: string = ""): Expectation<T> {
353
438
  if (!isString<T>()) ERROR("toMatch() can only be used on string types!");
354
439
  // @ts-ignore
355
440
  const passed = this._left.indexOf(value) >= 0;
@@ -361,36 +446,39 @@ export class Expectation<T> extends Tests {
361
446
  q(value),
362
447
  message,
363
448
  );
449
+ return this;
364
450
  }
365
451
 
366
452
  /**
367
453
  * Tests if a string starts with the provided prefix.
368
454
  */
369
- toStartWith(value: string, message: string = ""): void {
455
+ toStartWith(value: string, message: string = ""): Expectation<T> {
370
456
  if (!isString<T>())
371
457
  ERROR("toStartWith() can only be used on string types!");
372
458
  // @ts-ignore
373
459
  const left = this._left as string;
374
460
  const passed = left.indexOf(value) == 0;
375
461
  this._resolve(passed, "toStartWith", q(left), q(value), message);
462
+ return this;
376
463
  }
377
464
 
378
465
  /**
379
466
  * Tests if a string ends with the provided suffix.
380
467
  */
381
- toEndWith(value: string, message: string = ""): void {
468
+ toEndWith(value: string, message: string = ""): Expectation<T> {
382
469
  if (!isString<T>()) ERROR("toEndWith() can only be used on string types!");
383
470
  // @ts-ignore
384
471
  const left = this._left as string;
385
472
  const idx = left.lastIndexOf(value);
386
473
  const passed = idx >= 0 && idx + value.length == left.length;
387
474
  this._resolve(passed, "toEndWith", q(left), q(value), message);
475
+ return this;
388
476
  }
389
477
 
390
478
  /**
391
479
  * Tests if an array has length x
392
480
  */
393
- toHaveLength(value: i32, message: string = ""): void {
481
+ toHaveLength(value: i32, message: string = ""): Expectation<T> {
394
482
  // @ts-ignore
395
483
  const leftLen = this._left.length as i32;
396
484
  // @ts-ignore
@@ -402,13 +490,14 @@ export class Expectation<T> extends Tests {
402
490
  value.toString(),
403
491
  message,
404
492
  );
493
+ return this;
405
494
  }
406
495
 
407
496
  /**
408
497
  * Tests if an array or string contains a value
409
498
  */
410
499
  // @ts-ignore
411
- toContain(value: valueof<T>, message: string = ""): void {
500
+ toContain(value: valueof<T>, message: string = ""): Expectation<T> {
412
501
  if (isString<T>()) {
413
502
  // @ts-ignore
414
503
  const left = this._left as string;
@@ -416,7 +505,7 @@ export class Expectation<T> extends Tests {
416
505
  const needle = value as string;
417
506
  const passed = left.indexOf(needle) >= 0;
418
507
  this._resolve(passed, "toContain", q(left), q(needle), message);
419
- return;
508
+ return this;
420
509
  }
421
510
 
422
511
  if (isArray<T>()) {
@@ -425,35 +514,37 @@ export class Expectation<T> extends Tests {
425
514
  this._resolve(
426
515
  passed,
427
516
  "toContain",
428
- JSON.stringify<T>(this._left),
429
- JSON.stringify<valueof<T>>(value),
517
+ safeStringify<T>(this._left),
518
+ safeStringify<valueof<T>>(value),
430
519
  message,
431
520
  );
432
- return;
521
+ return this;
433
522
  }
434
523
 
435
524
  ERROR("toContain() can only be used on string and array types!");
525
+ return this;
436
526
  }
437
527
 
438
528
  /**
439
529
  * Alias for toContain().
440
530
  */
441
531
  // @ts-ignore
442
- toContains(value: valueof<T>, message: string = ""): void {
443
- this.toContain(value, message);
532
+ toContains(value: valueof<T>, message: string = ""): Expectation<T> {
533
+ return this.toContain(value, message);
444
534
  }
445
535
 
446
536
  /**
447
537
  * Tests if serialized value matches stored snapshot.
448
538
  */
449
- toMatchSnapshot(name: string = "", message: string = ""): void {
539
+ toMatchSnapshot(name: string = "", message: string = ""): Expectation<T> {
450
540
  let key = name.length
451
541
  ? namedSnapshotKey(this._snapshotKey, name)
452
542
  : nextUnnamedSnapshotKey(this._snapshotKey);
453
543
 
454
- const actual = JSON.stringify<T>(this._left);
544
+ const actual = safeStringify<T>(this._left);
455
545
  const res = snapshotAssert(key, actual);
456
546
  this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
547
+ return this;
457
548
  }
458
549
 
459
550
  /**
@@ -466,7 +557,7 @@ export class Expectation<T> extends Tests {
466
557
  * `.toThrow()` on a non-function value records a failure that explains the
467
558
  * usage.
468
559
  */
469
- toThrow(message: string = ""): void {
560
+ toThrow(message: string = ""): Expectation<T> {
470
561
  // @ts-ignore
471
562
  if (!isDefined(AS_TEST_TRY_AS)) {
472
563
  if (!warnedToThrowDisabled) {
@@ -476,7 +567,7 @@ export class Expectation<T> extends Tests {
476
567
  warnedToThrowDisabled = true;
477
568
  }
478
569
  this._resolve(true, "toThrow", q("disabled"), q("disabled"), message);
479
- return;
570
+ return this;
480
571
  }
481
572
 
482
573
  if (!isFunction<T>()) {
@@ -489,7 +580,7 @@ export class Expectation<T> extends Tests {
489
580
  ? message
490
581
  : "toThrow() requires a function: expect((): void => { ... }).toThrow()",
491
582
  );
492
- return;
583
+ return this;
493
584
  }
494
585
 
495
586
  // try-as rewrites the throw inside the callback to bump
@@ -515,111 +606,51 @@ export class Expectation<T> extends Tests {
515
606
  q("throws"),
516
607
  message,
517
608
  );
609
+ return this;
518
610
  }
519
611
 
520
612
  /**
521
613
  * Tests for equality
522
614
  */
523
- toBe(equals: T, message: string = ""): void {
524
- const passed = this._left === equals;
525
-
615
+ toBe(equals: T, message: string = ""): Expectation<T> {
616
+ // Deep structural equality for managed values; `===` semantics for
617
+ // primitives and strings (reflectEquals does the dispatch).
618
+ // `toEqual` is kept below as a Jest-familiarity alias.
619
+ const passed = reflectEquals<T>(this._left, equals, [], false);
526
620
  this._resolve(
527
621
  passed,
528
622
  "toBe",
529
- JSON.stringify<T>(this._left),
530
- JSON.stringify<T>(equals),
623
+ safeStringify<T>(this._left),
624
+ safeStringify<T>(equals),
531
625
  message,
532
626
  );
627
+ return this;
533
628
  }
534
629
 
535
630
  /**
536
- * Tests for deep equality
631
+ * Alias of `toBe` retained for Jest familiarity.
537
632
  */
538
- toEqual(equals: T, message: string = ""): void {
539
- const passed = valueEquals<T>(this._left, equals, false);
540
- this._resolve(
541
- passed,
542
- "toEqual",
543
- JSON.stringify<T>(this._left),
544
- JSON.stringify<T>(equals),
545
- message,
546
- );
633
+ toEqual(equals: T, message: string = ""): Expectation<T> {
634
+ return this.toBe(equals, message);
547
635
  }
548
636
 
549
637
  /**
550
- * Tests for strict deep equality
638
+ * Like `toBe` but also requires the runtime type (rtId) of the
639
+ * operands to match for managed values.
551
640
  */
552
- toStrictEqual(equals: T, message: string = ""): void {
553
- const passed = valueEquals<T>(this._left, equals, true);
641
+ toStrictEqual(equals: T, message: string = ""): Expectation<T> {
642
+ const passed = reflectEquals<T>(this._left, equals, [], true);
554
643
  this._resolve(
555
644
  passed,
556
645
  "toStrictEqual",
557
- JSON.stringify<T>(this._left),
558
- JSON.stringify<T>(equals),
646
+ safeStringify<T>(this._left),
647
+ safeStringify<T>(equals),
559
648
  message,
560
649
  );
650
+ return this;
561
651
  }
562
652
  }
563
653
 
564
- function arrayEquals<T>(a: T[], b: T[], strict: bool): boolean {
565
- if (a.length != b.length) return false;
566
- for (let i = 0; i < a.length; i++) {
567
- if (!valueEquals<T>(unchecked(a[i]), unchecked(b[i]), strict)) {
568
- return false;
569
- }
570
- }
571
- return true;
572
- }
573
-
574
- function valueEquals<T>(left: T, right: T, strict: bool): bool {
575
- if (isBoolean<T>() || isString<T>() || isInteger<T>() || isFloat<T>()) {
576
- return left === right;
577
- }
578
-
579
- if (isNullable<T>()) {
580
- const leftPtr = changetype<usize>(left);
581
- const rightPtr = changetype<usize>(right);
582
- if (leftPtr == 0 || rightPtr == 0) return leftPtr == rightPtr;
583
- }
584
-
585
- if (isArray<T>()) {
586
- return arrayEquals<valueof<T>>(
587
- changetype<valueof<T>[]>(left),
588
- changetype<valueof<T>[]>(right),
589
- strict,
590
- );
591
- }
592
-
593
- if (isManaged<T>()) {
594
- return managedEquals<T>(left, right, strict);
595
- }
596
-
597
- abort(
598
- `Unsupported equality matcher for ${nameof<T>()}. Use toBe() for identity or compare fields explicitly.`,
599
- );
600
- return false;
601
- }
602
-
603
- export function __as_test_deep_equal<T>(left: T, right: T, strict: bool): bool {
604
- return valueEquals<T>(left, right, strict);
605
- }
606
-
607
- function managedEquals<T>(left: T, right: T, strict: bool): bool {
608
- const leftPtr = changetype<usize>(left);
609
- const rightPtr = changetype<usize>(right);
610
- if (leftPtr == rightPtr) return true;
611
- if (leftPtr == 0 || rightPtr == 0) return false;
612
-
613
- if (strict) {
614
- const leftObject = changetype<OBJECT>(leftPtr - TOTAL_OVERHEAD);
615
- const rightObject = changetype<OBJECT>(rightPtr - TOTAL_OVERHEAD);
616
- if (leftObject.rtId != rightObject.rtId) return false;
617
- }
618
-
619
- // @ts-ignore
620
- return left.__as_test_equals(right, strict);
621
- }
622
-
623
654
  function isTruthy<T>(value: T): bool {
624
655
  if (isBoolean<T>()) {
625
656
  return value as bool;
@@ -641,5 +672,5 @@ function isTruthy<T>(value: T): bool {
641
672
  }
642
673
 
643
674
  function q(value: string): string {
644
- return JSON.stringify<string>(value);
675
+ return escape(value);
645
676
  }