@rcompat/test 0.11.2 → 0.12.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
@@ -5,8 +5,9 @@ Testing library for JavaScript runtimes.
5
5
  ## What is @rcompat/test?
6
6
 
7
7
  A cross-runtime testing library with a fluent assertion API. Provides deep
8
- equality checks, type assertions, and exception testing. Designed to work
9
- with the `proby` test runner. Works consistently across Node, Deno, and Bun.
8
+ equality checks, type assertions, exception testing, fetch interception, and
9
+ module mocking. Designed to work with the `proby` test runner. Works
10
+ consistently across Node, Deno, and Bun.
10
11
 
11
12
  ## Installation
12
13
 
@@ -114,12 +115,12 @@ test.case("throws", assert => {
114
115
  // check that function throws
115
116
  assert(() => {
116
117
  throw new Error("oops");
117
- }).throws();
118
+ }).throws("oops");
118
119
 
119
- // Check specific error message
120
+ // Check specific error type
120
121
  assert(() => {
121
- throw new Error("invalid input");
122
- }).throws("invalid input");
122
+ throw new TypeError("invalid input");
123
+ }).throws(TypeError);
123
124
  });
124
125
 
125
126
  test.case("tries (does not throw)", assert => {
@@ -158,11 +159,6 @@ test.case("async operations", async assert => {
158
159
  const result = await Promise.resolve(42);
159
160
  assert(result).equals(42);
160
161
  });
161
-
162
- test.case("fetch data", async assert => {
163
- const response = await fetch("https://api.example.com/data");
164
- assert(response.ok).true();
165
- });
166
162
  ```
167
163
 
168
164
  ### Cleanup with ended
@@ -177,11 +173,193 @@ test.case("database test", async assert => {
177
173
  });
178
174
 
179
175
  test.ended(async () => {
180
- // cleanup after all tests in this file
176
+ // runs after all tests in this file
181
177
  await closeDatabase();
182
178
  });
183
179
  ```
184
180
 
181
+ ### Grouping tests
182
+
183
+ Use `test.group` to cluster related test cases together. Groups can be run
184
+ individually via proby.
185
+
186
+ ```js
187
+ import test from "@rcompat/test";
188
+
189
+ test.group("addition", () => {
190
+ test.case("integers", assert => {
191
+ assert(1 + 1).equals(2);
192
+ });
193
+
194
+ test.case("floats", assert => {
195
+ assert(0.1 + 0.2).equals(0.3);
196
+ });
197
+ });
198
+
199
+ test.group("subtraction", () => {
200
+ test.case("integers", assert => {
201
+ assert(3 - 1).equals(2);
202
+ });
203
+ });
204
+ ```
205
+
206
+ Run a specific group:
207
+
208
+ ```bash
209
+ npx proby math.spec.ts addition
210
+ ```
211
+
212
+ ### Mocking modules
213
+
214
+ Use `test.mock` to replace a module's exports and `test.import` to import the
215
+ mocked module.
216
+
217
+ ```js
218
+ import test from "@rcompat/test";
219
+
220
+ using math = test.mock("./math.ts", () => ({
221
+ add: (a, b) => 99,
222
+ }));
223
+
224
+ const { add } = await test.import("./math.ts");
225
+
226
+ test.case("returns mocked value", assert => {
227
+ assert(add(1, 2)).equals(99);
228
+ });
229
+
230
+ test.case("tracks calls", assert => {
231
+ add(1, 2);
232
+ assert(math.add.calls.length).equals(1);
233
+ assert(math.add.calls[0]).equals([1, 2]);
234
+ assert(math.add.called).true();
235
+ });
236
+ ```
237
+
238
+ Tracked mock functions record each call as a tuple of arguments in `calls`.
239
+ Call tracking resets between test cases.
240
+
241
+ ### Static mocks with proby
242
+
243
+ When running tests with `proby`, you can preload mocks before a spec file is
244
+ evaluated by adding a sibling mock file.
245
+
246
+ - `foo.spec.ts` pairs with `foo.mock.ts`
247
+ - `foo.spec.js` pairs with `foo.mock.js`
248
+
249
+ `proby` loads the mock file before the spec file, so the spec's static imports
250
+ see the mocked module immediately.
251
+
252
+ ```ts
253
+ // math.ts
254
+ export function add(a: number, b: number) {
255
+ return a + b;
256
+ }
257
+ ```
258
+
259
+ ```ts
260
+ // math.mock.ts
261
+ import test from "@rcompat/test";
262
+
263
+ test.mock("./math.ts", () => ({
264
+ add: (a: number, b: number) => 99,
265
+ }));
266
+ ```
267
+
268
+ ```ts
269
+ // math.spec.ts
270
+ import test from "@rcompat/test";
271
+ import { add } from "./math.ts";
272
+
273
+ test.case("static mock is loaded before the spec", assert => {
274
+ assert(add(1, 2)).equals(99);
275
+ });
276
+ ```
277
+
278
+ Static mocks are file-scoped when run through `proby`; they do not leak into
279
+ later spec files.
280
+
281
+ ### Intercepting fetch
282
+
283
+ Use `test.intercept` to block outbound fetch calls to a specific origin and
284
+ replace them with fake responses. Calls to other origins pass through
285
+ untouched. The intercept records every request so you can assert on what was
286
+ called and how.
287
+
288
+ ```js
289
+ import test from "@rcompat/test";
290
+
291
+ await using telegram = test.intercept("https://api.telegram.org", setup => {
292
+ setup.post("/sendMessage", () => ({
293
+ ok: true,
294
+ result: { message_id: 42 },
295
+ }));
296
+ });
297
+
298
+ test.case("notifies user via telegram on signup", async assert => {
299
+ await fetch("http://localhost:6161/signup", {
300
+ method: "POST",
301
+ body: JSON.stringify({ email: "foo@bar.com" }),
302
+ });
303
+
304
+ // assert the path was hit the right number of times
305
+ assert(telegram.calls("/sendMessage")).equals(1);
306
+
307
+ // assert on the actual request that came in
308
+ assert(telegram.requests("/sendMessage")[0].method).equals("POST");
309
+ });
310
+ ```
311
+
312
+ `await using` restores the original fetch automatically when the file scope
313
+ exits. For long-lived intercepts that need manual control, use `restore()`
314
+ with `test.ended`:
315
+
316
+ ```js
317
+ const telegram = test.intercept("https://api.telegram.org", setup => {
318
+ setup.post("/sendMessage", () => ({ ok: true, result: { message_id: 42 } }));
319
+ });
320
+
321
+ test.case("first case", async assert => {
322
+ // ...
323
+ });
324
+
325
+ test.case("second case", async assert => {
326
+ // ...
327
+ });
328
+
329
+ test.ended(() => telegram.restore());
330
+ ```
331
+
332
+ Hitting a path on an intercepted origin that has no registered handler throws
333
+ immediately, catching accidental unhandled calls early.
334
+
335
+ ### Extending the asserter
336
+
337
+ Use `test.extend` to attach custom assertion methods to the asserter. Useful
338
+ for domain-specific assertions shared across many test cases.
339
+
340
+ ```js
341
+ import test from "@rcompat/test";
342
+
343
+ // create an extended test with custom assertions
344
+ const myTest = test.extend((assert, subject) => ({
345
+ even() {
346
+ const passed = subject % 2 === 0;
347
+ // use the base asserter to report the result
348
+ assert(passed).true();
349
+ return this;
350
+ },
351
+ }));
352
+
353
+ myTest.case("even numbers", assert => {
354
+ assert(2).even();
355
+ assert(4).even();
356
+ });
357
+ ```
358
+
359
+ The factory receives the base `assert` function and the current `subject`
360
+ (the value passed to `assert()`). Return an object whose methods will be
361
+ mixed into every `Assert` instance for that test.
362
+
185
363
  ## API Reference
186
364
 
187
365
  ### `test.case`
@@ -205,6 +383,99 @@ test.ended(callback: () => void | Promise<void>): void;
205
383
 
206
384
  Register a cleanup callback to run after all tests in the file.
207
385
 
386
+ ### `test.group`
387
+
388
+ ```ts
389
+ test.group(name: string, fn: () => void): void;
390
+ ```
391
+
392
+ Group test cases under a named scope. Groups can be targeted individually
393
+ when running proby.
394
+
395
+ | Parameter | Type | Description |
396
+ | --------- | ---------- | ----------------------------------- |
397
+ | `name` | `string` | Group name, used by proby to filter |
398
+ | `fn` | `function` | Function containing `test.case` calls |
399
+
400
+ ### `test.mock`
401
+
402
+ ```ts
403
+ test.mock<T extends object>(
404
+ specifier: string,
405
+ factory: (original: unknown) => T,
406
+ ): MockHandle<T>;
407
+ ```
408
+
409
+ Register a module mock and return a handle to the tracked mocked exports.
410
+ Function exports are wrapped so you can inspect `calls` and `called`.
411
+
412
+ | Parameter | Type | Description |
413
+ | ----------- | ---------- | -------------------------------------------- |
414
+ | `specifier` | `string` | Module specifier to mock |
415
+ | `factory` | `function` | Returns the mocked exports for that module |
416
+
417
+ ### `test.import`
418
+
419
+ ```ts
420
+ test.import(specifier: string): Promise<unknown>;
421
+ ```
422
+
423
+ Import a module after mocks have been registered.
424
+
425
+ ### `test.intercept`
426
+
427
+ ```ts
428
+ test.intercept(
429
+ base_url: string,
430
+ setup: (setup: Setup) => void
431
+ ): Intercept;
432
+ ```
433
+
434
+ Intercept outbound fetch calls to `base_url`. Returns an `Intercept` object
435
+ for asserting on recorded requests.
436
+
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 |
441
+
442
+ ### `test.extend`
443
+
444
+ ```ts
445
+ test.extend<Subject, Extensions>(
446
+ factory: (assert: Asserter, subject: Subject) => Extensions
447
+ ): ExtendedTest<Extensions>;
448
+ ```
449
+
450
+ Create a new test object with custom assertion methods mixed into the
451
+ asserter.
452
+
453
+ | Parameter | Type | Description |
454
+ | --------- | ---------- | ---------------------------------------------------------- |
455
+ | `factory` | `function` | Returns extra methods to attach to each `Assert` instance |
456
+
457
+ #### `Setup`
458
+
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 |
466
+
467
+ Each handler receives the incoming `Request` and returns a plain object,
468
+ which is serialized into a `Response` automatically.
469
+
470
+ #### `Intercept`
471
+
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.asyncDispose]` | Called automatically by `await using` |
478
+
208
479
  ### `Asserter`
209
480
 
210
481
  ```ts
@@ -219,15 +490,16 @@ The assert function passed to test cases.
219
490
  | ----------------------- | ----------------------------------------- |
220
491
  | `equals(expected)` | Deep equality check |
221
492
  | `nequals(expected)` | Deep inequality check |
493
+ | `includes(expected)` | Inclusion check (string, array, object) |
222
494
  | `true()` | Assert value is `true` |
223
495
  | `false()` | Assert value is `false` |
224
496
  | `null()` | Assert value is `null` |
225
497
  | `undefined()` | Assert value is `undefined` |
226
498
  | `defined()` | Assert value is not `undefined` |
227
499
  | `instance(constructor)` | Assert value is instance of class |
228
- | `throws(message?)` | Assert function throws (optional message) |
500
+ | `throws(expected?)` | Assert function throws |
229
501
  | `tries()` | Assert function does not throw |
230
- | `not` | Negate the next asertion |
502
+ | `not` | Negate the next assertion |
231
503
  | `type<T>()` | Compile-time type assertion |
232
504
  | `nottype<T>()` | Compile-time negative type assertion |
233
505
  | `pass()` | Manually pass the assertion |
@@ -273,7 +545,7 @@ import E from "@rcompat/test/E";
273
545
  E(error: unknown): { message: string };
274
546
  ```
275
547
 
276
- Extract error message from unknown error type.
548
+ Extract error data from unknown error types.
277
549
 
278
550
  ## Examples
279
551
 
@@ -301,17 +573,9 @@ import test from "@rcompat/test";
301
573
 
302
574
  class Calculator {
303
575
  #value = 0;
304
- add(n) {
305
- this.#value += n;
306
- return this;
307
- }
308
- subtract(n) {
309
- this.#value -= n;
310
- return this;
311
- }
312
- get value() {
313
- return this.#value;
314
- }
576
+ add(n) { this.#value += n; return this; }
577
+ subtract(n) { this.#value -= n; return this; }
578
+ get value() { return this.#value; }
315
579
  }
316
580
 
317
581
  test.case("calculator operations", assert => {
@@ -345,6 +609,24 @@ test.case("divide works normally", assert => {
345
609
  });
346
610
  ```
347
611
 
612
+ ### Testing code that calls external APIs
613
+
614
+ ```js
615
+ import test from "@rcompat/test";
616
+
617
+ await using openai = test.intercept("https://api.openai.com", setup => {
618
+ setup.post("/v1/chat/completions", () => ({
619
+ choices: [{ message: { content: "Hello!" } }],
620
+ }));
621
+ });
622
+
623
+ test.case("generates a reply", async assert => {
624
+ const reply = await myService.generateReply("hi");
625
+ assert(reply).equals("Hello!");
626
+ assert(openai.calls("/v1/chat/completions")).equals(1);
627
+ });
628
+ ```
629
+
348
630
  ## Cross-Runtime Compatibility
349
631
 
350
632
  | Runtime | Supported |
@@ -362,3 +644,4 @@ MIT
362
644
  ## Contributing
363
645
 
364
646
  See [CONTRIBUTING.md](../../CONTRIBUTING.md) in the repository root.
647
+
@@ -5,9 +5,10 @@ import type { FileRef } from "@rcompat/fs";
5
5
  export default class Suite {
6
6
  #private;
7
7
  constructor(file: FileRef);
8
- test(name: string, body: Body): void;
8
+ test(name: string, body: Body, group?: string): void;
9
9
  ended(end: End): void;
10
10
  get file(): FileRef;
11
+ between(fn: () => void): void;
11
12
  run(): AsyncGenerator<Test, void, unknown>;
12
13
  end(): Promise<void>;
13
14
  }
@@ -3,11 +3,12 @@ export default class Suite {
3
3
  #file;
4
4
  #tests = [];
5
5
  #ends = [];
6
+ #between = [];
6
7
  constructor(file) {
7
8
  this.#file = file;
8
9
  }
9
- test(name, body) {
10
- this.#tests.push(new Test(name, body));
10
+ test(name, body, group) {
11
+ this.#tests.push(new Test(name, body, group));
11
12
  }
12
13
  ended(end) {
13
14
  this.#ends.push(end);
@@ -15,9 +16,15 @@ export default class Suite {
15
16
  get file() {
16
17
  return this.#file;
17
18
  }
19
+ between(fn) {
20
+ this.#between.push(fn);
21
+ }
18
22
  async *run() {
19
23
  for (const test of this.#tests) {
20
24
  yield await test.run();
25
+ for (const fn of this.#between) {
26
+ fn();
27
+ }
21
28
  }
22
29
  }
23
30
  async end() {
@@ -2,8 +2,9 @@ import type Body from "#Body";
2
2
  import Result from "#Result";
3
3
  export default class Test {
4
4
  #private;
5
- constructor(name: string, body: Body);
5
+ constructor(name: string, body: Body, group?: string);
6
6
  get name(): string;
7
+ get group(): string | undefined;
7
8
  get results(): Result<unknown>[];
8
9
  report<T>(actual: T, expected: T, passed: boolean): void;
9
10
  run(): Promise<this>;
@@ -4,13 +4,18 @@ export default class Test {
4
4
  #name;
5
5
  #body;
6
6
  #results = [];
7
- constructor(name, body) {
7
+ #group;
8
+ constructor(name, body, group) {
8
9
  this.#name = name;
9
10
  this.#body = body;
11
+ this.#group = group;
10
12
  }
11
13
  get name() {
12
14
  return this.#name;
13
15
  }
16
+ get group() {
17
+ return this.#group;
18
+ }
14
19
  get results() {
15
20
  return this.#results;
16
21
  }
@@ -0,0 +1,2 @@
1
+ export declare function add(a: number, b: number): number;
2
+ //# sourceMappingURL=math.d.ts.map
@@ -0,0 +1,5 @@
1
+ export function add(a, b) {
2
+ return a + b;
3
+ }
4
+ ;
5
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1,2 @@
1
+ export default function mock_import(specifier: string): Promise<any>;
2
+ //# sourceMappingURL=import.d.ts.map
@@ -0,0 +1,4 @@
1
+ export default function mock_import(specifier) {
2
+ return import(`${specifier}?mock=${Date.now()}`);
3
+ }
4
+ //# sourceMappingURL=import.js.map
@@ -5,10 +5,27 @@ 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
+ import import_ from "#import";
8
10
  declare const _default: {
11
+ intercept: (base_url: string, setup: (setup: {
12
+ get(path: string, handler: (request: Request) => unknown): void;
13
+ post(path: string, handler: (request: Request) => unknown): void;
14
+ put(path: string, handler: (request: Request) => unknown): void;
15
+ patch(path: string, handler: (request: Request) => unknown): void;
16
+ delete(path: string, handler: (request: Request) => unknown): void;
17
+ }) => void) => {
18
+ calls(path: string): number;
19
+ requests(path: string): Request[];
20
+ restore(): void;
21
+ [Symbol.asyncDispose](): Promise<void>;
22
+ };
9
23
  extend<Subject, Extensions>(factory: Factory<Subject, Extensions>): ExtendedTest<Extensions>;
10
24
  case(name: string, body: Body): void;
11
25
  ended(end: End): void;
26
+ group(name: string, fn: () => void): void;
27
+ mock: typeof mock;
28
+ import: typeof import_;
12
29
  };
13
30
  export default _default;
14
31
  export type { Asserter, Env, ExtendedTest, Result, Test };
@@ -1,5 +1,8 @@
1
1
  import extend from "#extend";
2
2
  import repository from "#repository";
3
+ import intercept from "#intercept";
4
+ import mock from "#mock";
5
+ import import_ from "#import";
3
6
  const base = {
4
7
  case(name, body) {
5
8
  repository.put(name, body);
@@ -7,9 +10,15 @@ const base = {
7
10
  ended(end) {
8
11
  repository.ended(end);
9
12
  },
13
+ group(name, fn) {
14
+ repository.group(name, fn);
15
+ },
16
+ mock,
17
+ import: import_,
10
18
  };
11
19
  export default {
12
20
  ...base,
21
+ intercept,
13
22
  extend(factory) {
14
23
  return extend(base, factory);
15
24
  },
@@ -0,0 +1,17 @@
1
+ type Handler = (request: Request) => unknown;
2
+ type Setup = {
3
+ get(path: string, handler: Handler): void;
4
+ post(path: string, handler: Handler): void;
5
+ put(path: string, handler: Handler): void;
6
+ patch(path: string, handler: Handler): void;
7
+ delete(path: string, handler: Handler): void;
8
+ };
9
+ type Intercept = {
10
+ calls(path: string): number;
11
+ requests(path: string): Request[];
12
+ restore(): void;
13
+ [Symbol.asyncDispose](): Promise<void>;
14
+ };
15
+ declare const _default: (base_url: string, setup: (setup: Setup) => void) => Intercept;
16
+ export default _default;
17
+ //# sourceMappingURL=intercept.d.ts.map
@@ -0,0 +1,47 @@
1
+ export default (base_url, setup) => {
2
+ const routes = [];
3
+ const log = [];
4
+ const original = globalThis.fetch;
5
+ const register = (method) => (path, handler) => {
6
+ routes.push({ method, path, handler });
7
+ };
8
+ setup({
9
+ get: register("GET"),
10
+ post: register("POST"),
11
+ put: register("PUT"),
12
+ patch: register("PATCH"),
13
+ delete: register("DELETE"),
14
+ });
15
+ globalThis.fetch = (async (input, init) => {
16
+ const request = new Request(input, init);
17
+ const url = new URL(request.url);
18
+ const origin = url.origin;
19
+ const path = url.pathname;
20
+ if (origin !== base_url) {
21
+ return original(request);
22
+ }
23
+ const route = routes.find(r => r.method === request.method && r.path === path);
24
+ if (route === undefined) {
25
+ throw new Error(`no intercept registered for ${request.method} ${url.href}`);
26
+ }
27
+ log.push(request);
28
+ const body = route.handler(request);
29
+ return Response.json(body);
30
+ });
31
+ const restore = () => {
32
+ globalThis.fetch = original;
33
+ };
34
+ return {
35
+ calls(path) {
36
+ return log.filter(r => new URL(r.url).pathname === path).length;
37
+ },
38
+ requests(path) {
39
+ return log.filter(r => new URL(r.url).pathname === path);
40
+ },
41
+ restore,
42
+ async [Symbol.asyncDispose]() {
43
+ restore();
44
+ },
45
+ };
46
+ };
47
+ //# sourceMappingURL=intercept.js.map
@@ -0,0 +1,14 @@
1
+ type AnyFn = (...args: any[]) => any;
2
+ type Tracked<F extends AnyFn> = F & {
3
+ calls: Parameters<F>[];
4
+ readonly called: boolean;
5
+ };
6
+ type MockHandle<T extends object> = {
7
+ [K in keyof T]: T[K] extends AnyFn ? Tracked<T[K]> : T[K];
8
+ } & {
9
+ restore(): void;
10
+ [Symbol.dispose](): void;
11
+ };
12
+ export default function mock<T extends object>(specifier: string, factory: (original: unknown) => T): MockHandle<T>;
13
+ export {};
14
+ //# sourceMappingURL=mock.d.ts.map
@@ -0,0 +1,87 @@
1
+ import repository from "#repository";
2
+ import is from "@rcompat/is";
3
+ import module from "node:module";
4
+ let loader_registered = false;
5
+ const MARK = "std:test/mock";
6
+ const clean = (specifier) => specifier.split("?")[0];
7
+ const track = (fn) => {
8
+ const calls = [];
9
+ const tracked = ((...args) => {
10
+ calls.push(args);
11
+ return fn(...args);
12
+ });
13
+ tracked.calls = calls;
14
+ Object.defineProperty(tracked, "called", {
15
+ get: () => calls.length > 0,
16
+ });
17
+ return tracked;
18
+ };
19
+ const hooks = {
20
+ resolve(specifier, context, next) {
21
+ const id = clean(specifier);
22
+ if (!repository.mocks.has(id)) {
23
+ return next(specifier, context);
24
+ }
25
+ const resolved = next(id, context);
26
+ const url = new URL(resolved.url);
27
+ url.searchParams.set(MARK, id);
28
+ return {
29
+ ...resolved,
30
+ shortCircuit: true,
31
+ url: url.href,
32
+ };
33
+ },
34
+ load(url, context, next) {
35
+ const parsed = new URL(url);
36
+ const id = parsed.searchParams.get(MARK);
37
+ if (id === null) {
38
+ return next(url, context);
39
+ }
40
+ const tracked = repository.mocks.get(id);
41
+ if (tracked === undefined)
42
+ throw new Error(`mock not found for ${id}`);
43
+ const exports = Object.keys(tracked);
44
+ const source = `
45
+ import repository from "@rcompat/test/repository";
46
+ const tracked = repository.mocks.get(${JSON.stringify(id)});
47
+ if (tracked === undefined) {
48
+ throw new Error(${JSON.stringify(`mock not found for ${id}`)});
49
+ }
50
+ ${exports.map(k => `export const ${k} = tracked[${JSON.stringify(k)}];`).join("\n")}
51
+ `;
52
+ return {
53
+ format: "module",
54
+ shortCircuit: true,
55
+ source,
56
+ };
57
+ },
58
+ };
59
+ export default function mock(specifier, factory) {
60
+ if (!loader_registered) {
61
+ module.registerHooks(hooks);
62
+ loader_registered = true;
63
+ }
64
+ const id = clean(specifier);
65
+ const mocked = factory({});
66
+ const tracked = Object.fromEntries(Object.entries(mocked).map(([k, v]) => [
67
+ k,
68
+ is.function(v) ? track(v) : v,
69
+ ]));
70
+ repository.mocks.set(id, tracked);
71
+ repository.between(() => {
72
+ for (const v of Object.values(tracked)) {
73
+ if (is.function(v) && "calls" in v) {
74
+ v.calls.length = 0;
75
+ }
76
+ }
77
+ });
78
+ return Object.assign(tracked, {
79
+ restore() {
80
+ repository.mocks.delete(id);
81
+ },
82
+ [Symbol.dispose]() {
83
+ this.restore();
84
+ },
85
+ });
86
+ }
87
+ //# sourceMappingURL=mock.js.map
@@ -4,7 +4,10 @@ import Suite from "#Suite";
4
4
  import type { FileRef } from "@rcompat/fs";
5
5
  declare class Repository {
6
6
  #private;
7
+ get mocks(): Map<string, unknown>;
7
8
  put(name: string, body: Body): void;
9
+ group(name: string, fn: () => void): void;
10
+ between(fn: () => void): void;
8
11
  ended(end: End): void;
9
12
  suite(file: FileRef): void;
10
13
  reset(): void;
@@ -1,11 +1,24 @@
1
1
  import Suite from "#Suite";
2
2
  class Repository {
3
3
  #suites = [];
4
+ #current_group;
5
+ #mocks = new Map();
4
6
  get #suite() {
5
7
  return this.#suites.at(-1);
6
8
  }
9
+ get mocks() {
10
+ return this.#mocks;
11
+ }
7
12
  put(name, body) {
8
- this.#suite.test(name, body);
13
+ this.#suite.test(name, body, this.#current_group);
14
+ }
15
+ group(name, fn) {
16
+ this.#current_group = name;
17
+ fn();
18
+ this.#current_group = undefined;
19
+ }
20
+ between(fn) {
21
+ this.#suite.between(fn);
9
22
  }
10
23
  ended(end) {
11
24
  this.#suite.ended(end);
@@ -15,6 +28,7 @@ class Repository {
15
28
  }
16
29
  reset() {
17
30
  this.#suites = [];
31
+ this.#mocks = new Map();
18
32
  }
19
33
  *next() {
20
34
  for (const suite of this.#suites) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcompat/test",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Standard library testing",
5
5
  "bugs": "https://github.com/rcompat/rcompat/issues",
6
6
  "license": "MIT",
@@ -14,42 +14,45 @@
14
14
  "url": "https://github.com/rcompat/rcompat",
15
15
  "directory": "packages/test"
16
16
  },
17
+ "dependencies": {
18
+ "@rcompat/is": "^0.6.0"
19
+ },
17
20
  "devDependencies": {
18
- "@rcompat/fs": "^0.27.1",
19
- "@rcompat/type": "^0.10.0"
21
+ "@rcompat/fs": "^0.28.0",
22
+ "@rcompat/type": "^0.11.0"
20
23
  },
21
24
  "type": "module",
22
25
  "imports": {
23
26
  "#mask/*": {
24
- "apekit": "./src/private/mask/*.ts",
27
+ "source": "./src/private/mask/*.ts",
25
28
  "default": "./lib/private/mask/*.js"
26
29
  },
27
30
  "#types/*": {
28
- "apekit": "./src/private/types/*.ts",
31
+ "source": "./src/private/types/*.ts",
29
32
  "default": "./lib/private/types/*.js"
30
33
  },
31
34
  "#*": {
32
- "apekit": "./src/private/*.ts",
35
+ "source": "./src/private/*.ts",
33
36
  "default": "./lib/private/*.js"
34
37
  }
35
38
  },
36
39
  "exports": {
37
40
  "./mask/*": {
38
- "apekit": "./src/public/mask/*.ts",
41
+ "source": "./src/public/mask/*.ts",
39
42
  "default": "./lib/public/mask/*.js"
40
43
  },
41
44
  "./*": {
42
- "apekit": "./src/public/*.ts",
45
+ "source": "./src/public/*.ts",
43
46
  "default": "./lib/public/*.js"
44
47
  },
45
48
  ".": {
46
- "apekit": "./src/public/index.ts",
49
+ "source": "./src/public/index.ts",
47
50
  "default": "./lib/public/index.js"
48
51
  }
49
52
  },
50
53
  "scripts": {
51
54
  "build": "npm run clean && tsc",
52
- "test": "npm run build && npx proby",
55
+ "test": "npx proby",
53
56
  "clean": "rm -rf ./lib"
54
57
  }
55
58
  }