@rcompat/test 0.18.0 → 0.19.1

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,60 @@ 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 test from "@rcompat/test";
288
+
289
+ const add = (a: number, b: number) => a + b;
290
+ const tracked = test.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
+
305
+ test.case("tracks calls", assert => {
306
+ const tracked = test.spy((a: number, b: number) => a + b);
307
+
308
+ assert(tracked.called).false();
309
+ assert(tracked.calls).equals([]);
310
+
311
+ tracked(1, 2);
312
+
313
+ assert(tracked.called).true();
314
+ assert(tracked.calls).equals([[1, 2]]);
315
+ assert(tracked(3, 4)).equals(7);
316
+ });
317
+ ```
318
+
319
+ Pass a second argument to replace the implementation while still tracking calls:
320
+
321
+ ```ts
322
+ import test from "@rcompat/test";
323
+
324
+ test.case("mocks implementation", assert => {
325
+ const tracked = test.spy(
326
+ (a: number, b: number) => a + b,
327
+ (a: number, b: number) => a * b,
328
+ );
329
+
330
+ assert(tracked(2, 3)).equals(6); // mocker runs, not original
331
+ assert(tracked.calls).equals([[2, 3]]);
332
+ });
333
+ ```
334
+
281
335
  ### Intercepting fetch
282
336
 
283
337
  Use `test.intercept` to block outbound fetch calls to a specific origin and
@@ -392,9 +446,9 @@ test.group(name: string, fn: () => void): void;
392
446
  Group test cases under a named scope. Groups can be targeted individually
393
447
  when running proby.
394
448
 
395
- | Parameter | Type | Description |
396
- | --------- | ---------- | ----------------------------------- |
397
- | `name` | `string` | Group name, used by proby to filter |
449
+ | Parameter | Type | Description |
450
+ | --------- | ---------- | ------------------------------------- |
451
+ | `name` | `string` | Group name, used by proby to filter |
398
452
  | `fn` | `function` | Function containing `test.case` calls |
399
453
 
400
454
  ### `test.mock`
@@ -409,10 +463,10 @@ test.mock<T extends object>(
409
463
  Register a module mock and return a handle to the tracked mocked exports.
410
464
  Function exports are wrapped so you can inspect `calls` and `called`.
411
465
 
412
- | Parameter | Type | Description |
413
- | ----------- | ---------- | -------------------------------------------- |
414
- | `specifier` | `string` | Module specifier to mock |
415
- | `factory` | `function` | Returns the mocked exports for that module |
466
+ | Parameter | Type | Description |
467
+ | ----------- | ---------- | ------------------------------------------ |
468
+ | `specifier` | `string` | Module specifier to mock |
469
+ | `factory` | `function` | Returns the mocked exports for that module |
416
470
 
417
471
  ### `test.import`
418
472
 
@@ -434,10 +488,10 @@ test.intercept(
434
488
  Intercept outbound fetch calls to `base_url`. Returns an `Intercept` object
435
489
  for asserting on recorded requests.
436
490
 
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 |
491
+ | Parameter | Type | Description |
492
+ | ---------- | ---------- | ------------------------------------------------------ |
493
+ | `base_url` | `string` | Origin to intercept, e.g. `"https://api.example.com"` |
494
+ | `setup` | `function` | Register route handlers on the setup object |
441
495
 
442
496
  ### `test.extend`
443
497
 
@@ -450,31 +504,52 @@ test.extend<Subject, Extensions>(
450
504
  Create a new test object with custom assertion methods mixed into the
451
505
  asserter.
452
506
 
453
- | Parameter | Type | Description |
454
- | --------- | ---------- | ---------------------------------------------------------- |
455
- | `factory` | `function` | Returns extra methods to attach to each `Assert` instance |
507
+ | Parameter | Type | Description |
508
+ | --------- | ---------- | --------------------------------------------------------- |
509
+ | `factory` | `function` | Returns extra methods to attach to each `Assert` instance |
510
+
511
+ ### `test.spy`
512
+
513
+ ```ts
514
+ test.spy<F extends (...args: any[]) => any>(fn: F, mocker?: F): Tracked<F>;
515
+ ```
516
+
517
+ Wrap a function to track calls. Returns the wrapped function with two extra
518
+ properties.
519
+
520
+ | Parameter | Type | Description |
521
+ | --------- | ---- | ------------------------------------ |
522
+ | `fn` | `F` | The function to wrap |
523
+ | `mocker` | `F` | Optional replacement implementation |
524
+
525
+ #### `Tracked<F>`
526
+
527
+ | Property | Type | Description |
528
+ | -------- | ----------------- | -------------------------------------- |
529
+ | `calls` | `Parameters<F>[]` | Array of argument tuples, one per call |
530
+ | `called` | `boolean` | `true` if the function has been called |
456
531
 
457
532
  #### `Setup`
458
533
 
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 |
534
+ | Method | Description |
535
+ | ----------------------- | ------------------------- |
536
+ | `get(path, handler)` | Register a GET handler |
537
+ | `post(path, handler)` | Register a POST handler |
538
+ | `put(path, handler)` | Register a PUT handler |
539
+ | `patch(path, handler)` | Register a PATCH handler |
540
+ | `delete(path, handler)` | Register a DELETE handler |
466
541
 
467
542
  Each handler receives the incoming `Request` and returns a plain object,
468
543
  which is serialized into a `Response` automatically.
469
544
 
470
545
  #### `Intercept`
471
546
 
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` |
547
+ | Method | Description |
548
+ | ------------------ | ---------------------------------------------- |
549
+ | `calls(path)` | Number of times `path` was hit |
550
+ | `requests(path)` | Array of `Request` objects recorded for `path` |
551
+ | `restore()` | Reinstate the original `globalThis.fetch` |
552
+ | `[Symbol.dispose]` | Called automatically by `using` |
478
553
 
479
554
  ### `Asserter`
480
555
 
@@ -486,24 +561,24 @@ The assert function passed to test cases.
486
561
 
487
562
  ### `Assert<T>`
488
563
 
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 |
564
+ | Method | Description |
565
+ | ----------------------- | --------------------------------------- |
566
+ | `equals(expected)` | Deep equality check |
567
+ | `nequals(expected)` | Deep inequality check |
568
+ | `includes(expected)` | Inclusion check (string, array, object) |
569
+ | `true()` | Assert value is `true` |
570
+ | `false()` | Assert value is `false` |
571
+ | `null()` | Assert value is `null` |
572
+ | `undefined()` | Assert value is `undefined` |
573
+ | `defined()` | Assert value is not `undefined` |
574
+ | `instance(constructor)` | Assert value is instance of class |
575
+ | `throws(expected?)` | Assert function throws |
576
+ | `tries()` | Assert function does not throw |
577
+ | `not` | Negate the next assertion |
578
+ | `type<T>()` | Compile-time type assertion |
579
+ | `nottype<T>()` | Compile-time negative type assertion |
580
+ | `pass()` | Manually pass the assertion |
581
+ | `fail(reason?)` | Manually fail the assertion |
507
582
 
508
583
  ### Utilities
509
584
 
@@ -644,4 +719,3 @@ MIT
644
719
  ## Contributing
645
720
 
646
721
  See [CONTRIBUTING.md](../../CONTRIBUTING.md) in the repository root.
647
-
@@ -2,6 +2,6 @@ type EncodedError = {
2
2
  message: string;
3
3
  code?: string;
4
4
  };
5
- declare const _default: (error: unknown) => EncodedError;
6
5
  export default _default;
6
+ declare function _default(error: unknown): EncodedError;
7
7
  //# sourceMappingURL=E.d.ts.map
@@ -14,6 +14,6 @@ type Base = {
14
14
  case(name: string, body: Body): void;
15
15
  ended(end: () => MaybePromise<void>): void;
16
16
  };
17
- declare const _default: <Subject, Extensions>(base: Base, factory: Factory<Subject, Extensions>) => ExtendedTest<Extensions>;
18
17
  export default _default;
18
+ declare function _default<Subject, Extensions>(base: Base, factory: Factory<Subject, Extensions>): ExtendedTest<Extensions>;
19
19
  //# sourceMappingURL=extend.d.ts.map
@@ -5,13 +5,15 @@ 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: {
11
12
  case(name: string, body: Body): void;
12
13
  ended(end: End): void;
13
14
  group(name: string, fn: () => void): void;
14
15
  mock: typeof mock;
16
+ spy: typeof spy;
15
17
  import: typeof import_;
16
18
  intercept: (base_url: string, setup: (setup: {
17
19
  get(path: string, handler: (request: Request) => unknown): void;
@@ -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 {
@@ -12,6 +12,6 @@ type Intercept = {
12
12
  restore(): void;
13
13
  [Symbol.dispose](): void;
14
14
  };
15
- declare const _default: (base_url: string, setup: (setup: Setup) => void) => Intercept;
16
15
  export default _default;
16
+ declare function _default(base_url: string, setup: (setup: Setup) => void): Intercept;
17
17
  //# sourceMappingURL=intercept.d.ts.map
@@ -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
@@ -1,6 +1,6 @@
1
1
  import type { UnknownMap } from "@rcompat/type";
2
- declare const _default: (map: UnknownMap) => {
2
+ export default _default;
3
+ declare function _default(map: UnknownMap): {
3
4
  [k: string]: unknown;
4
5
  };
5
- export default _default;
6
6
  //# sourceMappingURL=to-object.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcompat/test",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Standard library testing",
5
5
  "bugs": "https://github.com/rcompat/rcompat/issues",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "@rcompat/is": "^0.12.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@rcompat/fs": "^0.35.0",
21
+ "@rcompat/fs": "^0.35.1",
22
22
  "@rcompat/type": "^0.18.0"
23
23
  },
24
24
  "type": "module",