as-test 1.1.10 → 1.3.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.
@@ -57,6 +57,41 @@ export class Expectation<T> extends Tests {
57
57
  return this;
58
58
  }
59
59
 
60
+ /**
61
+ * Asserts that a custom predicate holds. Accepts either a bool or a
62
+ * `() => bool` lambda. Useful when the verdict isn't expressible via the
63
+ * built-in matchers — e.g. delegating to a hand-written comparator.
64
+ *
65
+ * expect(x).where(x > 0 && x < 10);
66
+ * expect(actual).where((): bool => deepEqual(GLOBAL_A, GLOBAL_B));
67
+ *
68
+ * Chains with other matchers as an independent assertion:
69
+ *
70
+ * expect(7).toBe(7).where((): bool => isFresh());
71
+ *
72
+ * Note: AssemblyScript does not implement closures, so the lambda cannot
73
+ * capture local variables from the enclosing scope. Use the bool form when
74
+ * the predicate references locals, or refer to module-level values from
75
+ * inside the lambda.
76
+ */
77
+ where<W>(predicate: W, message: string = ""): Expectation<T> {
78
+ let passed: bool;
79
+ if (isFunction<W>()) {
80
+ passed = (predicate as () => bool)();
81
+ } else {
82
+ // @ts-ignore: W is a bool-compatible primitive in this branch
83
+ passed = predicate as bool;
84
+ }
85
+ this._resolve(
86
+ passed,
87
+ "where",
88
+ q(passed ? "true" : "false"),
89
+ q("true"),
90
+ message,
91
+ );
92
+ return this;
93
+ }
94
+
60
95
  private _resolve(
61
96
  passed: bool,
62
97
  instr: string,
@@ -74,14 +109,26 @@ export class Expectation<T> extends Tests {
74
109
  return;
75
110
  }
76
111
  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
112
  const resolvedMessage = message.length ? message : this._message;
82
- this.message = isFail ? resolvedMessage : "";
113
+ // When matchers chain, later ones must not overwrite an earlier failure's
114
+ // recorded state — otherwise a passing matcher after a failed one would
115
+ // flip the suite's verdict back to "ok". Each matcher still fires its own
116
+ // IPC failure independently below.
117
+ if (this.verdict != "fail") {
118
+ this.verdict = isFail ? "fail" : "ok";
119
+ this.instr = instr;
120
+ this.left = left;
121
+ this.right = right;
122
+ this.message = isFail ? resolvedMessage : "";
123
+ }
83
124
  if (isFail) {
84
- sendAssertionFailure(this._snapshotKey, instr, left, right, this.message);
125
+ sendAssertionFailure(
126
+ this._snapshotKey,
127
+ instr,
128
+ left,
129
+ right,
130
+ resolvedMessage,
131
+ );
85
132
  // @ts-ignore
86
133
  if (isDefined(AS_TEST_FUZZ)) {
87
134
  // @ts-ignore
@@ -95,7 +142,7 @@ export class Expectation<T> extends Tests {
95
142
  // @ts-ignore
96
143
  __as_test_fuzz_failure_right = right;
97
144
  // @ts-ignore
98
- __as_test_fuzz_failure_message = this.message;
145
+ __as_test_fuzz_failure_message = resolvedMessage;
99
146
  }
100
147
  }
101
148
  }
@@ -105,7 +152,7 @@ export class Expectation<T> extends Tests {
105
152
  /**
106
153
  * Tests if a == null
107
154
  */
108
- toBeNull(message: string = ""): void {
155
+ toBeNull(message: string = ""): Expectation<T> {
109
156
  const passed =
110
157
  (isNullable<T>() && changetype<usize>(this._left) == 0) ||
111
158
  (isInteger<T>() && nameof<T>() == "usize" && this._left == 0);
@@ -121,12 +168,13 @@ export class Expectation<T> extends Tests {
121
168
  ),
122
169
  message,
123
170
  );
171
+ return this;
124
172
  }
125
173
 
126
174
  /**
127
175
  * Tests if a > b
128
176
  */
129
- toBeGreaterThan(value: T, message: string = ""): void {
177
+ toBeGreaterThan(value: T, message: string = ""): Expectation<T> {
130
178
  if (!isInteger<T>() && !isFloat<T>())
131
179
  ERROR("toBeGreaterThan() can only be used on number types!");
132
180
 
@@ -144,12 +192,13 @@ export class Expectation<T> extends Tests {
144
192
  ),
145
193
  message,
146
194
  );
195
+ return this;
147
196
  }
148
197
 
149
198
  /**
150
199
  * Tests if a >= b
151
200
  */
152
- toBeGreaterOrEqualTo(value: T, message: string = ""): void {
201
+ toBeGreaterOrEqualTo(value: T, message: string = ""): Expectation<T> {
153
202
  if (!isInteger<T>() && !isFloat<T>())
154
203
  ERROR("toBeGreaterOrEqualTo() can only be used on number types!");
155
204
 
@@ -167,12 +216,13 @@ export class Expectation<T> extends Tests {
167
216
  ),
168
217
  message,
169
218
  );
219
+ return this;
170
220
  }
171
221
 
172
222
  /**
173
223
  * Tests if a < b
174
224
  */
175
- toBeLessThan(value: T, message: string = ""): void {
225
+ toBeLessThan(value: T, message: string = ""): Expectation<T> {
176
226
  if (!isInteger<T>() && !isFloat<T>())
177
227
  ERROR("toBeLessThan() can only be used on number types!");
178
228
 
@@ -190,12 +240,13 @@ export class Expectation<T> extends Tests {
190
240
  ),
191
241
  message,
192
242
  );
243
+ return this;
193
244
  }
194
245
 
195
246
  /**
196
247
  * Tests if a <= b
197
248
  */
198
- toBeLessThanOrEqualTo(value: T, message: string = ""): void {
249
+ toBeLessThanOrEqualTo(value: T, message: string = ""): Expectation<T> {
199
250
  if (!isInteger<T>() && !isFloat<T>())
200
251
  ERROR("toBeLessThanOrEqualTo() can only be used on number types!");
201
252
 
@@ -213,12 +264,13 @@ export class Expectation<T> extends Tests {
213
264
  ),
214
265
  message,
215
266
  );
267
+ return this;
216
268
  }
217
269
 
218
270
  /**
219
271
  * Tests if a is string
220
272
  */
221
- toBeString(message: string = ""): void {
273
+ toBeString(message: string = ""): Expectation<T> {
222
274
  this._resolve(
223
275
  isString<T>(),
224
276
  "toBeString",
@@ -226,12 +278,13 @@ export class Expectation<T> extends Tests {
226
278
  q("string"),
227
279
  message,
228
280
  );
281
+ return this;
229
282
  }
230
283
 
231
284
  /**
232
285
  * Tests if a is boolean
233
286
  */
234
- toBeBoolean(message: string = ""): void {
287
+ toBeBoolean(message: string = ""): Expectation<T> {
235
288
  this._resolve(
236
289
  isBoolean<T>(),
237
290
  "toBeBoolean",
@@ -239,12 +292,13 @@ export class Expectation<T> extends Tests {
239
292
  q("boolean"),
240
293
  message,
241
294
  );
295
+ return this;
242
296
  }
243
297
 
244
298
  /**
245
299
  * Tests if a is array
246
300
  */
247
- toBeArray(message: string = ""): void {
301
+ toBeArray(message: string = ""): Expectation<T> {
248
302
  this._resolve(
249
303
  isArray<T>(),
250
304
  "toBeArray",
@@ -252,12 +306,13 @@ export class Expectation<T> extends Tests {
252
306
  q("Array<any>"),
253
307
  message,
254
308
  );
309
+ return this;
255
310
  }
256
311
 
257
312
  /**
258
313
  * Tests if a is number
259
314
  */
260
- toBeNumber(message: string = ""): void {
315
+ toBeNumber(message: string = ""): Expectation<T> {
261
316
  this._resolve(
262
317
  isFloat<T>() || isInteger<T>(),
263
318
  "toBeNumber",
@@ -265,12 +320,13 @@ export class Expectation<T> extends Tests {
265
320
  q("number"),
266
321
  message,
267
322
  );
323
+ return this;
268
324
  }
269
325
 
270
326
  /**
271
327
  * Tests if a is integer
272
328
  */
273
- toBeInteger(message: string = ""): void {
329
+ toBeInteger(message: string = ""): Expectation<T> {
274
330
  this._resolve(
275
331
  isInteger<T>(),
276
332
  "toBeInteger",
@@ -278,12 +334,13 @@ export class Expectation<T> extends Tests {
278
334
  q("integer"),
279
335
  message,
280
336
  );
337
+ return this;
281
338
  }
282
339
 
283
340
  /**
284
341
  * Tests if a is float
285
342
  */
286
- toBeFloat(message: string = ""): void {
343
+ toBeFloat(message: string = ""): Expectation<T> {
287
344
  this._resolve(
288
345
  isFloat<T>(),
289
346
  "toBeFloat",
@@ -291,21 +348,23 @@ export class Expectation<T> extends Tests {
291
348
  q("float"),
292
349
  message,
293
350
  );
351
+ return this;
294
352
  }
295
353
 
296
354
  /**
297
355
  * Tests if a is finite
298
356
  */
299
- toBeFinite(message: string = ""): void {
357
+ toBeFinite(message: string = ""): Expectation<T> {
300
358
  // @ts-ignore
301
359
  const passed = (isFloat<T>() || isInteger<T>()) && isFinite(this._left);
302
360
  this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"), message);
361
+ return this;
303
362
  }
304
363
 
305
364
  /**
306
365
  * Tests if a value is truthy
307
366
  */
308
- toBeTruthy(message: string = ""): void {
367
+ toBeTruthy(message: string = ""): Expectation<T> {
309
368
  this._resolve(
310
369
  isTruthy<T>(this._left),
311
370
  "toBeTruthy",
@@ -313,12 +372,13 @@ export class Expectation<T> extends Tests {
313
372
  q("truthy"),
314
373
  message,
315
374
  );
375
+ return this;
316
376
  }
317
377
 
318
378
  /**
319
379
  * Tests if a value is falsy
320
380
  */
321
- toBeFalsy(message: string = ""): void {
381
+ toBeFalsy(message: string = ""): Expectation<T> {
322
382
  this._resolve(
323
383
  !isTruthy<T>(this._left),
324
384
  "toBeFalsy",
@@ -326,12 +386,14 @@ export class Expectation<T> extends Tests {
326
386
  q("falsy"),
327
387
  message,
328
388
  );
389
+ return this;
329
390
  }
330
391
 
331
392
  /**
332
393
  * Tests if a floating-point number is close to expected
333
394
  */
334
- toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): void {
395
+ // prettier-ignore
396
+ toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): Expectation<T> {
335
397
  if (!isFloat<T>() && !isInteger<T>())
336
398
  ERROR("toBeCloseTo() can only be used on number types!");
337
399
  const factor = Math.pow(10, precision as f64);
@@ -344,12 +406,13 @@ export class Expectation<T> extends Tests {
344
406
  visualize<T>(expected),
345
407
  message,
346
408
  );
409
+ return this;
347
410
  }
348
411
 
349
412
  /**
350
413
  * Tests if a string contains substring
351
414
  */
352
- toMatch(value: string, message: string = ""): void {
415
+ toMatch(value: string, message: string = ""): Expectation<T> {
353
416
  if (!isString<T>()) ERROR("toMatch() can only be used on string types!");
354
417
  // @ts-ignore
355
418
  const passed = this._left.indexOf(value) >= 0;
@@ -361,36 +424,39 @@ export class Expectation<T> extends Tests {
361
424
  q(value),
362
425
  message,
363
426
  );
427
+ return this;
364
428
  }
365
429
 
366
430
  /**
367
431
  * Tests if a string starts with the provided prefix.
368
432
  */
369
- toStartWith(value: string, message: string = ""): void {
433
+ toStartWith(value: string, message: string = ""): Expectation<T> {
370
434
  if (!isString<T>())
371
435
  ERROR("toStartWith() can only be used on string types!");
372
436
  // @ts-ignore
373
437
  const left = this._left as string;
374
438
  const passed = left.indexOf(value) == 0;
375
439
  this._resolve(passed, "toStartWith", q(left), q(value), message);
440
+ return this;
376
441
  }
377
442
 
378
443
  /**
379
444
  * Tests if a string ends with the provided suffix.
380
445
  */
381
- toEndWith(value: string, message: string = ""): void {
446
+ toEndWith(value: string, message: string = ""): Expectation<T> {
382
447
  if (!isString<T>()) ERROR("toEndWith() can only be used on string types!");
383
448
  // @ts-ignore
384
449
  const left = this._left as string;
385
450
  const idx = left.lastIndexOf(value);
386
451
  const passed = idx >= 0 && idx + value.length == left.length;
387
452
  this._resolve(passed, "toEndWith", q(left), q(value), message);
453
+ return this;
388
454
  }
389
455
 
390
456
  /**
391
457
  * Tests if an array has length x
392
458
  */
393
- toHaveLength(value: i32, message: string = ""): void {
459
+ toHaveLength(value: i32, message: string = ""): Expectation<T> {
394
460
  // @ts-ignore
395
461
  const leftLen = this._left.length as i32;
396
462
  // @ts-ignore
@@ -402,13 +468,14 @@ export class Expectation<T> extends Tests {
402
468
  value.toString(),
403
469
  message,
404
470
  );
471
+ return this;
405
472
  }
406
473
 
407
474
  /**
408
475
  * Tests if an array or string contains a value
409
476
  */
410
477
  // @ts-ignore
411
- toContain(value: valueof<T>, message: string = ""): void {
478
+ toContain(value: valueof<T>, message: string = ""): Expectation<T> {
412
479
  if (isString<T>()) {
413
480
  // @ts-ignore
414
481
  const left = this._left as string;
@@ -416,7 +483,7 @@ export class Expectation<T> extends Tests {
416
483
  const needle = value as string;
417
484
  const passed = left.indexOf(needle) >= 0;
418
485
  this._resolve(passed, "toContain", q(left), q(needle), message);
419
- return;
486
+ return this;
420
487
  }
421
488
 
422
489
  if (isArray<T>()) {
@@ -429,24 +496,25 @@ export class Expectation<T> extends Tests {
429
496
  JSON.stringify<valueof<T>>(value),
430
497
  message,
431
498
  );
432
- return;
499
+ return this;
433
500
  }
434
501
 
435
502
  ERROR("toContain() can only be used on string and array types!");
503
+ return this;
436
504
  }
437
505
 
438
506
  /**
439
507
  * Alias for toContain().
440
508
  */
441
509
  // @ts-ignore
442
- toContains(value: valueof<T>, message: string = ""): void {
443
- this.toContain(value, message);
510
+ toContains(value: valueof<T>, message: string = ""): Expectation<T> {
511
+ return this.toContain(value, message);
444
512
  }
445
513
 
446
514
  /**
447
515
  * Tests if serialized value matches stored snapshot.
448
516
  */
449
- toMatchSnapshot(name: string = "", message: string = ""): void {
517
+ toMatchSnapshot(name: string = "", message: string = ""): Expectation<T> {
450
518
  let key = name.length
451
519
  ? namedSnapshotKey(this._snapshotKey, name)
452
520
  : nextUnnamedSnapshotKey(this._snapshotKey);
@@ -454,38 +522,75 @@ export class Expectation<T> extends Tests {
454
522
  const actual = JSON.stringify<T>(this._left);
455
523
  const res = snapshotAssert(key, actual);
456
524
  this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
525
+ return this;
457
526
  }
458
527
 
459
528
  /**
460
- * Delegates throw assertions to try-as when available.
461
- * If try-as is unavailable, this matcher is disabled and warns once.
529
+ * Invokes the wrapped function inside a try/catch and asserts it threw.
530
+ * Requires the try-as feature (`--enable try-as`).
531
+ *
532
+ * expect((): void => { throw new Error("boom"); }).toThrow();
533
+ *
534
+ * The value passed to `expect()` must be a `() => void` callback — calling
535
+ * `.toThrow()` on a non-function value records a failure that explains the
536
+ * usage.
462
537
  */
463
- toThrow(message: string = ""): void {
538
+ toThrow(message: string = ""): Expectation<T> {
464
539
  // @ts-ignore
465
540
  if (!isDefined(AS_TEST_TRY_AS)) {
466
541
  if (!warnedToThrowDisabled) {
467
542
  sendWarning(
468
- 'toThrow() is disabled because try-as is not installed. Install and import "try-as" to enable it.',
543
+ "toThrow() requires the try-as feature. Enable with --enable try-as.",
469
544
  );
470
545
  warnedToThrowDisabled = true;
471
546
  }
472
547
  this._resolve(true, "toThrow", q("disabled"), q("disabled"), message);
473
- return;
548
+ return this;
549
+ }
550
+
551
+ if (!isFunction<T>()) {
552
+ this._resolve(
553
+ false,
554
+ "toThrow",
555
+ q("non-function"),
556
+ q("() => void"),
557
+ message.length
558
+ ? message
559
+ : "toThrow() requires a function: expect((): void => { ... }).toThrow()",
560
+ );
561
+ return this;
474
562
  }
475
563
 
564
+ // try-as rewrites the throw inside the callback to bump
565
+ // __ExceptionState.Failures and return early from the arrow. We never
566
+ // wrap the call in try/catch here because try-as's source linker does not
567
+ // follow chained method calls (`expect(...).toThrow()`) and so it would
568
+ // not rewrite a `try` placed in this method body. Compare the failure
569
+ // counter before/after instead and consume any failure we observed.
570
+ // @ts-ignore: __ExceptionState is provided by the try-as transform
571
+ const beforeFailures = __ExceptionState.Failures;
572
+ // @ts-ignore: guarded by isFunction<T>() above
573
+ (this._left as () => void)();
476
574
  // @ts-ignore
477
- const passed = __ExceptionState.Failures > 0;
478
- if (passed) {
575
+ const threw = __ExceptionState.Failures > beforeFailures;
576
+ if (threw) {
479
577
  // @ts-ignore
480
- __ExceptionState.Failures--;
578
+ __ExceptionState.Failures = beforeFailures;
481
579
  }
482
- this._resolve(passed, "toThrow", q("throws"), q("throws"), message);
580
+ this._resolve(
581
+ threw,
582
+ "toThrow",
583
+ q(threw ? "threw" : "did not throw"),
584
+ q("throws"),
585
+ message,
586
+ );
587
+ return this;
483
588
  }
484
589
 
485
590
  /**
486
591
  * Tests for equality
487
592
  */
488
- toBe(equals: T, message: string = ""): void {
593
+ toBe(equals: T, message: string = ""): Expectation<T> {
489
594
  const passed = this._left === equals;
490
595
 
491
596
  this._resolve(
@@ -495,12 +600,13 @@ export class Expectation<T> extends Tests {
495
600
  JSON.stringify<T>(equals),
496
601
  message,
497
602
  );
603
+ return this;
498
604
  }
499
605
 
500
606
  /**
501
607
  * Tests for deep equality
502
608
  */
503
- toEqual(equals: T, message: string = ""): void {
609
+ toEqual(equals: T, message: string = ""): Expectation<T> {
504
610
  const passed = valueEquals<T>(this._left, equals, false);
505
611
  this._resolve(
506
612
  passed,
@@ -509,12 +615,13 @@ export class Expectation<T> extends Tests {
509
615
  JSON.stringify<T>(equals),
510
616
  message,
511
617
  );
618
+ return this;
512
619
  }
513
620
 
514
621
  /**
515
622
  * Tests for strict deep equality
516
623
  */
517
- toStrictEqual(equals: T, message: string = ""): void {
624
+ toStrictEqual(equals: T, message: string = ""): Expectation<T> {
518
625
  const passed = valueEquals<T>(this._left, equals, true);
519
626
  this._resolve(
520
627
  passed,
@@ -523,6 +630,7 @@ export class Expectation<T> extends Tests {
523
630
  JSON.stringify<T>(equals),
524
631
  message,
525
632
  );
633
+ return this;
526
634
  }
527
635
  }
528
636
 
@@ -0,0 +1,55 @@
1
+ // The current mode name is patched at build time by as-test's transform
2
+ // (transform/src/index.ts) using the AS_TEST_MODE_NAME env var that
3
+ // build-core.ts injects per-mode build. When unset, defaults to "default".
4
+ export const AS_TEST_MODE_NAME: string = "default";
5
+
6
+ function modeMatches(matchers: string[], current: string): bool {
7
+ if (matchers.length == 0) return false;
8
+ let sawPositive = false;
9
+ let positiveHit = false;
10
+ for (let i = 0; i < matchers.length; i++) {
11
+ const m = matchers[i];
12
+ if (m.length == 0) continue;
13
+ if (m.charCodeAt(0) == 33 /* '!' */) {
14
+ if (m.substring(1) == current) return false;
15
+ } else {
16
+ sawPositive = true;
17
+ if (m == current) positiveHit = true;
18
+ }
19
+ }
20
+ return sawPositive ? positiveHit : true;
21
+ }
22
+
23
+ /**
24
+ * Gate a block of suite/test registrations on the current execution mode.
25
+ *
26
+ * The current mode is the name under `modes.<name>` in `as-test.config.json`
27
+ * (or `"default"` when running the base config). Comparisons are by exact
28
+ * string match.
29
+ *
30
+ * Matcher semantics:
31
+ * - `["a"]` runs when the current mode equals `"a"`.
32
+ * - `["a", "b"]` runs when the current mode is in `{a, b}` (positive OR).
33
+ * - `["!a"]` runs when the current mode is NOT `"a"`.
34
+ * - `["!a", "!b"]` runs when the current mode is neither `{a, b}` (AND).
35
+ * - Mixed `["a", "!b"]` runs when any positive matches AND no negative does.
36
+ * - `[]` and empty entries are skipped (no-op).
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * mode(["simd"], () => {
41
+ * describe("vectorised path", () => { ... });
42
+ * });
43
+ *
44
+ * mode(["simd", "swar"], () => {
45
+ * describe("fast paths", () => { ... });
46
+ * });
47
+ *
48
+ * mode(["!naive"], () => {
49
+ * describe("anything but naive", () => { ... });
50
+ * });
51
+ * ```
52
+ */
53
+ export function mode(matchers: string[], fn: () => void): void {
54
+ if (modeMatches(matchers, AS_TEST_MODE_NAME)) fn();
55
+ }