@rcompat/test 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -278,6 +278,62 @@ test.case("static mock is loaded before the spec", assert => {
278
278
  Static mocks are file-scoped when run through `proby`; they do not leak into
279
279
  later spec files.
280
280
 
281
+ ### Spying on functions
282
+
283
+ Use `spy` to wrap a function and record every call made to it. The wrapped
284
+ function behaves identically to the original but tracks its arguments.
285
+
286
+ ```ts
287
+ import spy from "@rcompat/spy";
288
+
289
+ const add = (a: number, b: number) => a + b;
290
+ const tracked = spy(add);
291
+
292
+ tracked(1, 2);
293
+ tracked(3, 4);
294
+
295
+ tracked.called; // true
296
+ tracked.calls; // [[1, 2], [3, 4]]
297
+ tracked.calls[0]; // [1, 2]
298
+ ```
299
+
300
+ Use `spy` inside a test case to make call-tracking assertions:
301
+
302
+ ```ts
303
+ import test from "@rcompat/test";
304
+ import spy from "@rcompat/spy";
305
+
306
+ test.case("tracks calls", assert => {
307
+ const tracked = spy((a: number, b: number) => a + b);
308
+
309
+ assert(tracked.called).false();
310
+ assert(tracked.calls).equals([]);
311
+
312
+ tracked(1, 2);
313
+
314
+ assert(tracked.called).true();
315
+ assert(tracked.calls).equals([[1, 2]]);
316
+ assert(tracked(3, 4)).equals(7);
317
+ });
318
+ ```
319
+
320
+ Pass a second argument to replace the implementation while still tracking calls:
321
+
322
+ ```ts
323
+ import test from "@rcompat/test";
324
+ import spy from "@rcompat/spy";
325
+
326
+ test.case("mocks implementation", assert => {
327
+ const tracked = spy(
328
+ (a: number, b: number) => a + b,
329
+ (a: number, b: number) => a * b,
330
+ );
331
+
332
+ assert(tracked(2, 3)).equals(6); // mocker runs, not original
333
+ assert(tracked.calls).equals([[2, 3]]);
334
+ });
335
+ ```
336
+
281
337
  ### Intercepting fetch
282
338
 
283
339
  Use `test.intercept` to block outbound fetch calls to a specific origin and
@@ -392,9 +448,9 @@ test.group(name: string, fn: () => void): void;
392
448
  Group test cases under a named scope. Groups can be targeted individually
393
449
  when running proby.
394
450
 
395
- | Parameter | Type | Description |
396
- | --------- | ---------- | ----------------------------------- |
397
- | `name` | `string` | Group name, used by proby to filter |
451
+ | Parameter | Type | Description |
452
+ | --------- | ---------- | ------------------------------------- |
453
+ | `name` | `string` | Group name, used by proby to filter |
398
454
  | `fn` | `function` | Function containing `test.case` calls |
399
455
 
400
456
  ### `test.mock`
@@ -409,10 +465,10 @@ test.mock<T extends object>(
409
465
  Register a module mock and return a handle to the tracked mocked exports.
410
466
  Function exports are wrapped so you can inspect `calls` and `called`.
411
467
 
412
- | Parameter | Type | Description |
413
- | ----------- | ---------- | -------------------------------------------- |
414
- | `specifier` | `string` | Module specifier to mock |
415
- | `factory` | `function` | Returns the mocked exports for that module |
468
+ | Parameter | Type | Description |
469
+ | ----------- | ---------- | ------------------------------------------ |
470
+ | `specifier` | `string` | Module specifier to mock |
471
+ | `factory` | `function` | Returns the mocked exports for that module |
416
472
 
417
473
  ### `test.import`
418
474
 
@@ -434,10 +490,10 @@ test.intercept(
434
490
  Intercept outbound fetch calls to `base_url`. Returns an `Intercept` object
435
491
  for asserting on recorded requests.
436
492
 
437
- | Parameter | Type | Description |
438
- | ---------- | ---------- | --------------------------------------------------------- |
439
- | `base_url` | `string` | Origin to intercept, e.g. `"https://api.example.com"` |
440
- | `setup` | `function` | Register route handlers on the setup object |
493
+ | Parameter | Type | Description |
494
+ | ---------- | ---------- | ------------------------------------------------------ |
495
+ | `base_url` | `string` | Origin to intercept, e.g. `"https://api.example.com"` |
496
+ | `setup` | `function` | Register route handlers on the setup object |
441
497
 
442
498
  ### `test.extend`
443
499
 
@@ -450,31 +506,54 @@ test.extend<Subject, Extensions>(
450
506
  Create a new test object with custom assertion methods mixed into the
451
507
  asserter.
452
508
 
453
- | Parameter | Type | Description |
454
- | --------- | ---------- | ---------------------------------------------------------- |
455
- | `factory` | `function` | Returns extra methods to attach to each `Assert` instance |
509
+ | Parameter | Type | Description |
510
+ | --------- | ---------- | --------------------------------------------------------- |
511
+ | `factory` | `function` | Returns extra methods to attach to each `Assert` instance |
512
+
513
+ ### `spy`
514
+
515
+ ```ts
516
+ import spy from "@rcompat/spy";
517
+
518
+ spy<F extends (...args: any[]) => any>(fn: F, mocker?: F): Tracked<F>;
519
+ ```
520
+
521
+ Wrap a function to track calls. Returns the wrapped function with two extra
522
+ properties.
523
+
524
+ | Parameter | Type | Description |
525
+ | --------- | ---- | ------------------------------------ |
526
+ | `fn` | `F` | The function to wrap |
527
+ | `mocker` | `F` | Optional replacement implementation |
528
+
529
+ #### `Tracked<F>`
530
+
531
+ | Property | Type | Description |
532
+ | -------- | ----------------- | -------------------------------------- |
533
+ | `calls` | `Parameters<F>[]` | Array of argument tuples, one per call |
534
+ | `called` | `boolean` | `true` if the function has been called |
456
535
 
457
536
  #### `Setup`
458
537
 
459
- | Method | Description |
460
- | ---------------------- | ------------------------- |
461
- | `get(path, handler)` | Register a GET handler |
462
- | `post(path, handler)` | Register a POST handler |
463
- | `put(path, handler)` | Register a PUT handler |
464
- | `patch(path, handler)` | Register a PATCH handler |
465
- | `delete(path, handler)`| Register a DELETE handler |
538
+ | Method | Description |
539
+ | ----------------------- | ------------------------- |
540
+ | `get(path, handler)` | Register a GET handler |
541
+ | `post(path, handler)` | Register a POST handler |
542
+ | `put(path, handler)` | Register a PUT handler |
543
+ | `patch(path, handler)` | Register a PATCH handler |
544
+ | `delete(path, handler)` | Register a DELETE handler |
466
545
 
467
546
  Each handler receives the incoming `Request` and returns a plain object,
468
547
  which is serialized into a `Response` automatically.
469
548
 
470
549
  #### `Intercept`
471
550
 
472
- | Method | Description |
473
- | ----------------------- | ----------------------------------------------- |
474
- | `calls(path)` | Number of times `path` was hit |
475
- | `requests(path)` | Array of `Request` objects recorded for `path` |
476
- | `restore()` | Reinstate the original `globalThis.fetch` |
477
- | `[Symbol.dispose]` | Called automatically by `using` |
551
+ | Method | Description |
552
+ | ------------------ | ---------------------------------------------- |
553
+ | `calls(path)` | Number of times `path` was hit |
554
+ | `requests(path)` | Array of `Request` objects recorded for `path` |
555
+ | `restore()` | Reinstate the original `globalThis.fetch` |
556
+ | `[Symbol.dispose]` | Called automatically by `using` |
478
557
 
479
558
  ### `Asserter`
480
559
 
@@ -486,24 +565,24 @@ The assert function passed to test cases.
486
565
 
487
566
  ### `Assert<T>`
488
567
 
489
- | Method | Description |
490
- | ----------------------- | ----------------------------------------- |
491
- | `equals(expected)` | Deep equality check |
492
- | `nequals(expected)` | Deep inequality check |
493
- | `includes(expected)` | Inclusion check (string, array, object) |
494
- | `true()` | Assert value is `true` |
495
- | `false()` | Assert value is `false` |
496
- | `null()` | Assert value is `null` |
497
- | `undefined()` | Assert value is `undefined` |
498
- | `defined()` | Assert value is not `undefined` |
499
- | `instance(constructor)` | Assert value is instance of class |
500
- | `throws(expected?)` | Assert function throws |
501
- | `tries()` | Assert function does not throw |
502
- | `not` | Negate the next assertion |
503
- | `type<T>()` | Compile-time type assertion |
504
- | `nottype<T>()` | Compile-time negative type assertion |
505
- | `pass()` | Manually pass the assertion |
506
- | `fail(reason?)` | Manually fail the assertion |
568
+ | Method | Description |
569
+ | ----------------------- | --------------------------------------- |
570
+ | `equals(expected)` | Deep equality check |
571
+ | `nequals(expected)` | Deep inequality check |
572
+ | `includes(expected)` | Inclusion check (string, array, object) |
573
+ | `true()` | Assert value is `true` |
574
+ | `false()` | Assert value is `false` |
575
+ | `null()` | Assert value is `null` |
576
+ | `undefined()` | Assert value is `undefined` |
577
+ | `defined()` | Assert value is not `undefined` |
578
+ | `instance(constructor)` | Assert value is instance of class |
579
+ | `throws(expected?)` | Assert function throws |
580
+ | `tries()` | Assert function does not throw |
581
+ | `not` | Negate the next assertion |
582
+ | `type<T>()` | Compile-time type assertion |
583
+ | `nottype<T>()` | Compile-time negative type assertion |
584
+ | `pass()` | Manually pass the assertion |
585
+ | `fail(reason?)` | Manually fail the assertion |
507
586
 
508
587
  ### Utilities
509
588
 
@@ -644,4 +723,3 @@ MIT
644
723
  ## Contributing
645
724
 
646
725
  See [CONTRIBUTING.md](../../CONTRIBUTING.md) in the repository root.
647
-
@@ -1,6 +1,6 @@
1
1
  import type Test from "#Test";
2
2
  import type { Not, Print, UnknownFunction } from "@rcompat/type";
3
- type Equals<X, Y> = (<T>() => T extends X ? true : false) extends <T>() => T extends Y ? true : false ? true : false;
3
+ type Equals<X, Y> = (<T>() => T extends X ? true : false) extends <T>() => (T extends Y ? true : false) ? true : false;
4
4
  export default class Assert<const Actual> {
5
5
  #private;
6
6
  constructor(test: Test, actual?: Actual);
@@ -5,9 +5,16 @@ import type Env from "#Env";
5
5
  import type Result from "#Result";
6
6
  import type Test from "#Test";
7
7
  import type { ExtendedTest, Factory } from "#extend";
8
- import mock from "#mock";
9
8
  import import_ from "#import";
9
+ import mock from "#mock";
10
+ import spy from "#spy";
10
11
  declare const _default: {
12
+ case(name: string, body: Body): void;
13
+ ended(end: End): void;
14
+ group(name: string, fn: () => void): void;
15
+ mock: typeof mock;
16
+ spy: typeof spy;
17
+ import: typeof import_;
11
18
  intercept: (base_url: string, setup: (setup: {
12
19
  get(path: string, handler: (request: Request) => unknown): void;
13
20
  post(path: string, handler: (request: Request) => unknown): void;
@@ -21,11 +28,6 @@ declare const _default: {
21
28
  [Symbol.dispose](): void;
22
29
  };
23
30
  extend<Subject, Extensions>(factory: Factory<Subject, Extensions>): ExtendedTest<Extensions>;
24
- case(name: string, body: Body): void;
25
- ended(end: End): void;
26
- group(name: string, fn: () => void): void;
27
- mock: typeof mock;
28
- import: typeof import_;
29
31
  };
30
32
  export default _default;
31
33
  export type { Asserter, Env, ExtendedTest, Result, Test };
@@ -1,8 +1,9 @@
1
1
  import extend from "#extend";
2
- import repository from "#repository";
2
+ import import_ from "#import";
3
3
  import intercept from "#intercept";
4
4
  import mock from "#mock";
5
- import import_ from "#import";
5
+ import repository from "#repository";
6
+ import spy from "#spy";
6
7
  const base = {
7
8
  case(name, body) {
8
9
  repository.put(name, body);
@@ -14,6 +15,7 @@ const base = {
14
15
  repository.group(name, fn);
15
16
  },
16
17
  mock,
18
+ spy,
17
19
  import: import_,
18
20
  };
19
21
  export default {
@@ -0,0 +1,8 @@
1
+ type AnyFunction = (...args: any[]) => any;
2
+ type Tracked<F extends AnyFunction> = F & {
3
+ calls: Parameters<F>[];
4
+ called: boolean;
5
+ };
6
+ export default function spy<F extends AnyFunction>(fn: F, mocker?: F): Tracked<F>;
7
+ export {};
8
+ //# sourceMappingURL=spy.d.ts.map
@@ -0,0 +1,15 @@
1
+ import is from "@rcompat/is";
2
+ export default function spy(fn, mocker) {
3
+ const calls = [];
4
+ const callee = is.defined(mocker) ? mocker : fn;
5
+ const tracked = ((...args) => {
6
+ calls.push(args);
7
+ return callee(...args);
8
+ });
9
+ tracked.calls = calls;
10
+ Object.defineProperty(tracked, "called", {
11
+ get: () => calls.length > 0,
12
+ });
13
+ return tracked;
14
+ }
15
+ //# sourceMappingURL=spy.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcompat/test",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Standard library testing",
5
5
  "bugs": "https://github.com/rcompat/rcompat/issues",
6
6
  "license": "MIT",
@@ -15,11 +15,11 @@
15
15
  "directory": "packages/test"
16
16
  },
17
17
  "dependencies": {
18
- "@rcompat/is": "^0.11.0"
18
+ "@rcompat/is": "^0.12.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@rcompat/fs": "^0.33.0",
22
- "@rcompat/type": "^0.17.0"
21
+ "@rcompat/fs": "^0.35.1",
22
+ "@rcompat/type": "^0.18.0"
23
23
  },
24
24
  "type": "module",
25
25
  "imports": {
@@ -51,8 +51,9 @@
51
51
  }
52
52
  },
53
53
  "scripts": {
54
- "build": "npm run clean && tsc",
54
+ "build": "npm run clean && tsgo",
55
55
  "test": "npx proby",
56
- "clean": "rm -rf ./lib"
56
+ "clean": "rm -rf ./lib",
57
+ "lint": "eslint ."
57
58
  }
58
59
  }