@r0hitsharma/http-client-msw 0.12.0-rohit-fork-ci.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 +251 -0
- package/dist/base-url.d.ts +46 -0
- package/dist/base-url.js +65 -0
- package/dist/browser.d.ts +50 -0
- package/dist/browser.js +59 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/mock-api.d.ts +75 -0
- package/dist/mock-api.js +35 -0
- package/dist/mock-delay.d.ts +35 -0
- package/dist/mock-delay.js +50 -0
- package/dist/mock-store.d.ts +76 -0
- package/dist/mock-store.js +114 -0
- package/dist/node.d.ts +41 -0
- package/dist/node.js +41 -0
- package/dist/seeded-rng.d.ts +27 -0
- package/dist/seeded-rng.js +52 -0
- package/dist/setup.d.ts +54 -0
- package/dist/setup.js +26 -0
- package/dist/worker-options.d.ts +55 -0
- package/dist/worker-options.js +49 -0
- package/package.json +59 -0
- package/src/base-url.test.ts +177 -0
- package/src/base-url.ts +80 -0
- package/src/browser.ts +88 -0
- package/src/index.ts +53 -0
- package/src/mock-api.test.ts +148 -0
- package/src/mock-api.ts +96 -0
- package/src/mock-api.types.test.ts +119 -0
- package/src/mock-delay.test.ts +51 -0
- package/src/mock-delay.ts +68 -0
- package/src/mock-store.test.ts +263 -0
- package/src/mock-store.ts +180 -0
- package/src/node.ts +61 -0
- package/src/seeded-rng.test.ts +106 -0
- package/src/seeded-rng.ts +77 -0
- package/src/setup.test.ts +137 -0
- package/src/setup.ts +71 -0
- package/src/test-fixtures.ts +113 -0
- package/src/worker-options.test.ts +128 -0
- package/src/worker-options.ts +108 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latency is what makes a mock exercise the states a real API forces an app
|
|
3
|
+
* through — pending spinners, skeletons, optimistic UI, race conditions. The same
|
|
4
|
+
* latency in a test suite is dead time, so the amount is per-environment and the
|
|
5
|
+
* default under test is none.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* True when running under a test runner: `NODE_ENV === 'test'` (vitest, jest) or
|
|
9
|
+
* Vite's `MODE === 'test'`. Read defensively because neither `process` nor
|
|
10
|
+
* `import.meta.env` exists in every environment this package runs in.
|
|
11
|
+
*/
|
|
12
|
+
export function isTestEnvironment() {
|
|
13
|
+
// `process.env` is optional-chained as well as guarded: a browser shim that
|
|
14
|
+
// defines `process` without an `env` would otherwise throw from inside a
|
|
15
|
+
// request handler. The dot form is deliberate — it is what bundlers rewrite.
|
|
16
|
+
const nodeEnv = typeof process === 'undefined' ? undefined : process.env?.NODE_ENV;
|
|
17
|
+
const viteMode = import.meta.env
|
|
18
|
+
?.MODE;
|
|
19
|
+
return nodeEnv === 'test' || viteMode === 'test';
|
|
20
|
+
}
|
|
21
|
+
/** The pure resolution `mockDelay` applies, split out so both branches are testable. */
|
|
22
|
+
export function resolveMockDelay(input, isTest) {
|
|
23
|
+
if (typeof input === 'number')
|
|
24
|
+
return isTest ? 0 : input;
|
|
25
|
+
return (isTest ? input.test : input.dev) ?? 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Waits, in dev; resolves immediately under test unless a test delay is asked
|
|
29
|
+
* for.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* mock.get('/things', async ({ response }) => {
|
|
33
|
+
* await mockDelay(400);
|
|
34
|
+
* return response(200).json(things.list());
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // Keep a little latency under test, for a pending-state assertion.
|
|
38
|
+
* await mockDelay({ dev: 400, test: 10 });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function mockDelay(input = 0) {
|
|
42
|
+
const ms = resolveMockDelay(input, isTestEnvironment());
|
|
43
|
+
// No timer at all for a zero delay: a mock that resolves in the same
|
|
44
|
+
// microtask keeps a suite's timing behaviour unchanged.
|
|
45
|
+
if (ms <= 0)
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
setTimeout(resolve, ms);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest amount of state a mock needs to behave like an API rather than a
|
|
3
|
+
* fixture dump: a POST that shows up in the following GET. Deliberately not a
|
|
4
|
+
* database — no queries, no indexes, no relations. A mock that needs those is a
|
|
5
|
+
* mock standing in for logic the real service owns.
|
|
6
|
+
*/
|
|
7
|
+
export type MockStoreOptions<T> = {
|
|
8
|
+
/**
|
|
9
|
+
* How an item's identity is read. Defaults to its `id` property, which must be
|
|
10
|
+
* a string or a number.
|
|
11
|
+
*/
|
|
12
|
+
id?: (item: T) => string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Aliasing, since it is observable: `get`, `insert`, and the elements of
|
|
16
|
+
* `list` are the **stored** objects, so mutating one writes through to the
|
|
17
|
+
* store — only `list`'s array is a copy. `update` replaces the item with
|
|
18
|
+
* a merged copy instead, which leaves any reference taken before the update
|
|
19
|
+
* stale. Read again after an update rather than holding a reference across one.
|
|
20
|
+
*/
|
|
21
|
+
export type MockStore<T> = {
|
|
22
|
+
/**
|
|
23
|
+
* Every item, in insertion order. The array is fresh, so sorting, slicing, or
|
|
24
|
+
* splicing it cannot reorder the store — but its elements are the stored
|
|
25
|
+
* objects, not copies, so mutating one writes through like `get` does.
|
|
26
|
+
*/
|
|
27
|
+
list: () => T[];
|
|
28
|
+
get: (id: string) => T | undefined;
|
|
29
|
+
has: (id: string) => boolean;
|
|
30
|
+
readonly size: number;
|
|
31
|
+
/** Adds an item. Throws on a duplicate id, which is a fixture bug rather than an API condition. */
|
|
32
|
+
insert: (item: T) => T;
|
|
33
|
+
/**
|
|
34
|
+
* Merges `patch` into an existing item, keeping its position. `undefined`
|
|
35
|
+
* when the id is unknown — a handler turns that into its own 404.
|
|
36
|
+
*
|
|
37
|
+
* A patch may carry the id, which moves the item to it; the old id then
|
|
38
|
+
* misses. Throws if that id is already taken, like `insert`, and leaves the
|
|
39
|
+
* store untouched when it does.
|
|
40
|
+
*/
|
|
41
|
+
update: (id: string, patch: Partial<T>) => T | undefined;
|
|
42
|
+
/** `false` when the id is unknown. */
|
|
43
|
+
remove: (id: string) => boolean;
|
|
44
|
+
/** Swaps in a whole collection. Throws if two items share an id, like `insert`. */
|
|
45
|
+
replaceAll: (items: readonly T[]) => void;
|
|
46
|
+
/** Re-seeds from the seed function. Pass this to `setupMocks({ onReset })`. */
|
|
47
|
+
reset: () => void;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* An in-memory collection whose `reset` restores the seeded contents.
|
|
51
|
+
*
|
|
52
|
+
* The seed is a **function**, called on construction and again on every reset, so
|
|
53
|
+
* a handler that mutates an item in place cannot corrupt the next test's
|
|
54
|
+
* starting point. That holds only if the function *constructs* its items — a
|
|
55
|
+
* seed that returns a shared module-level array hands out the same objects every
|
|
56
|
+
* time, and a mutation to one of those does survive a reset.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const things = createMockStore(seedThings);
|
|
60
|
+
*
|
|
61
|
+
* export const mocks = setupMocks(
|
|
62
|
+
* [
|
|
63
|
+
* mock.get('/things', ({ response }) => response(200).json(things.list())),
|
|
64
|
+
* mock.get('/things/{id}', ({ params, response }) => {
|
|
65
|
+
* const thing = things.get(params.id);
|
|
66
|
+
*
|
|
67
|
+
* return thing
|
|
68
|
+
* ? response(200).json(thing)
|
|
69
|
+
* : response(404).json({ message: 'not found' });
|
|
70
|
+
* }),
|
|
71
|
+
* ],
|
|
72
|
+
* { onReset: [things.reset] },
|
|
73
|
+
* );
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function createMockStore<T>(seed: () => readonly T[], options?: MockStoreOptions<T>): MockStore<T>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest amount of state a mock needs to behave like an API rather than a
|
|
3
|
+
* fixture dump: a POST that shows up in the following GET. Deliberately not a
|
|
4
|
+
* database — no queries, no indexes, no relations. A mock that needs those is a
|
|
5
|
+
* mock standing in for logic the real service owns.
|
|
6
|
+
*/
|
|
7
|
+
function defaultIdOf(item) {
|
|
8
|
+
const id = item.id;
|
|
9
|
+
if (typeof id === 'string')
|
|
10
|
+
return id;
|
|
11
|
+
if (typeof id === 'number')
|
|
12
|
+
return String(id);
|
|
13
|
+
throw new Error('http-client-msw: createMockStore items need a string or number `id`, or an explicit `id` selector');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* An in-memory collection whose `reset` restores the seeded contents.
|
|
17
|
+
*
|
|
18
|
+
* The seed is a **function**, called on construction and again on every reset, so
|
|
19
|
+
* a handler that mutates an item in place cannot corrupt the next test's
|
|
20
|
+
* starting point. That holds only if the function *constructs* its items — a
|
|
21
|
+
* seed that returns a shared module-level array hands out the same objects every
|
|
22
|
+
* time, and a mutation to one of those does survive a reset.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const things = createMockStore(seedThings);
|
|
26
|
+
*
|
|
27
|
+
* export const mocks = setupMocks(
|
|
28
|
+
* [
|
|
29
|
+
* mock.get('/things', ({ response }) => response(200).json(things.list())),
|
|
30
|
+
* mock.get('/things/{id}', ({ params, response }) => {
|
|
31
|
+
* const thing = things.get(params.id);
|
|
32
|
+
*
|
|
33
|
+
* return thing
|
|
34
|
+
* ? response(200).json(thing)
|
|
35
|
+
* : response(404).json({ message: 'not found' });
|
|
36
|
+
* }),
|
|
37
|
+
* ],
|
|
38
|
+
* { onReset: [things.reset] },
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function createMockStore(seed, options = {}) {
|
|
43
|
+
const idOf = options.id ?? defaultIdOf;
|
|
44
|
+
const items = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Built into a staging map first, so the duplicate-id check that `insert`
|
|
47
|
+
* enforces also covers construction and every reset — and so a rejected
|
|
48
|
+
* collection leaves the store as it was rather than half-cleared.
|
|
49
|
+
*/
|
|
50
|
+
const replaceAll = (next) => {
|
|
51
|
+
const replacement = new Map();
|
|
52
|
+
for (const item of next) {
|
|
53
|
+
const id = idOf(item);
|
|
54
|
+
if (replacement.has(id)) {
|
|
55
|
+
throw new Error(`http-client-msw: createMockStore was given two items with id "${id}"`);
|
|
56
|
+
}
|
|
57
|
+
replacement.set(id, item);
|
|
58
|
+
}
|
|
59
|
+
items.clear();
|
|
60
|
+
for (const [id, item] of replacement)
|
|
61
|
+
items.set(id, item);
|
|
62
|
+
};
|
|
63
|
+
replaceAll(seed());
|
|
64
|
+
return {
|
|
65
|
+
list: () => [...items.values()],
|
|
66
|
+
get: (id) => items.get(id),
|
|
67
|
+
has: (id) => items.has(id),
|
|
68
|
+
get size() {
|
|
69
|
+
return items.size;
|
|
70
|
+
},
|
|
71
|
+
insert: (item) => {
|
|
72
|
+
const id = idOf(item);
|
|
73
|
+
if (items.has(id)) {
|
|
74
|
+
throw new Error(`http-client-msw: createMockStore already holds an item with id "${id}"`);
|
|
75
|
+
}
|
|
76
|
+
items.set(id, item);
|
|
77
|
+
return item;
|
|
78
|
+
},
|
|
79
|
+
update: (id, patch) => {
|
|
80
|
+
const existing = items.get(id);
|
|
81
|
+
if (existing === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const updated = { ...existing, ...patch };
|
|
84
|
+
// `Partial<T>` admits the id, so the patch can move the item. Re-derived
|
|
85
|
+
// rather than assumed, because leaving it under the old key desyncs the
|
|
86
|
+
// map from the items in it: `get(item.id)` misses and `get(oldId)` hands
|
|
87
|
+
// back an item that no longer claims that id.
|
|
88
|
+
const nextId = idOf(updated);
|
|
89
|
+
if (nextId === id) {
|
|
90
|
+
items.set(id, updated);
|
|
91
|
+
return updated;
|
|
92
|
+
}
|
|
93
|
+
if (items.has(nextId)) {
|
|
94
|
+
throw new Error(`http-client-msw: createMockStore already holds an item with id "${nextId}"`);
|
|
95
|
+
}
|
|
96
|
+
// Rebuilt in order rather than delete-then-set, which would move the item
|
|
97
|
+
// to the end: `update` keeps an item's position, and a patched id is
|
|
98
|
+
// still an update. The collision above throws before anything is
|
|
99
|
+
// touched, so a rejected patch leaves the store as it was.
|
|
100
|
+
const entries = [...items];
|
|
101
|
+
items.clear();
|
|
102
|
+
for (const [key, item] of entries) {
|
|
103
|
+
if (key === id)
|
|
104
|
+
items.set(nextId, updated);
|
|
105
|
+
else
|
|
106
|
+
items.set(key, item);
|
|
107
|
+
}
|
|
108
|
+
return updated;
|
|
109
|
+
},
|
|
110
|
+
remove: (id) => items.delete(id),
|
|
111
|
+
replaceAll,
|
|
112
|
+
reset: () => replaceAll(seed()),
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node entry: `@r0hitsharma/http-client-msw/node`.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own subpath so an app bundle never resolves `msw/node`, whose
|
|
5
|
+
* interceptors patch node's http modules.
|
|
6
|
+
*/
|
|
7
|
+
import { type SetupServer } from 'msw/node';
|
|
8
|
+
import type { MockSetup } from './setup.js';
|
|
9
|
+
export type MockServerOptions = NonNullable<Parameters<SetupServer['listen']>[0]>;
|
|
10
|
+
export type MockServer = {
|
|
11
|
+
/** The underlying msw server, for `use()` and lifecycle events. */
|
|
12
|
+
server: SetupServer;
|
|
13
|
+
/** Starts interception. `onUnhandledRequest` defaults to `'error'`. */
|
|
14
|
+
listen: (overrides?: MockServerOptions) => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
/** Runs the setup's state resets, then drops runtime handler overrides. */
|
|
17
|
+
reset: () => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Serves a {@link MockSetup} from msw's node interceptors — the same handler
|
|
21
|
+
* array a browser worker serves.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* // src/test-setup.ts
|
|
25
|
+
* import { setupMockServer } from '@r0hitsharma/http-client-msw/node';
|
|
26
|
+
* import { afterAll, afterEach, beforeAll } from 'vitest';
|
|
27
|
+
*
|
|
28
|
+
* import { mocks } from './mocks';
|
|
29
|
+
*
|
|
30
|
+
* const mockServer = setupMockServer(mocks);
|
|
31
|
+
*
|
|
32
|
+
* beforeAll(() => mockServer.listen());
|
|
33
|
+
* afterEach(() => mockServer.reset());
|
|
34
|
+
* afterAll(() => mockServer.close());
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* `onUnhandledRequest` defaults to `'error'` rather than msw's `'warn'`: in a
|
|
38
|
+
* test suite an unmocked request is a hole in the fixtures, and a warning that
|
|
39
|
+
* scrolls past is how a test ends up asserting against real network output.
|
|
40
|
+
*/
|
|
41
|
+
export declare function setupMockServer(mocks: MockSetup): MockServer;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node entry: `@r0hitsharma/http-client-msw/node`.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own subpath so an app bundle never resolves `msw/node`, whose
|
|
5
|
+
* interceptors patch node's http modules.
|
|
6
|
+
*/
|
|
7
|
+
import { setupServer } from 'msw/node';
|
|
8
|
+
/**
|
|
9
|
+
* Serves a {@link MockSetup} from msw's node interceptors — the same handler
|
|
10
|
+
* array a browser worker serves.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // src/test-setup.ts
|
|
14
|
+
* import { setupMockServer } from '@r0hitsharma/http-client-msw/node';
|
|
15
|
+
* import { afterAll, afterEach, beforeAll } from 'vitest';
|
|
16
|
+
*
|
|
17
|
+
* import { mocks } from './mocks';
|
|
18
|
+
*
|
|
19
|
+
* const mockServer = setupMockServer(mocks);
|
|
20
|
+
*
|
|
21
|
+
* beforeAll(() => mockServer.listen());
|
|
22
|
+
* afterEach(() => mockServer.reset());
|
|
23
|
+
* afterAll(() => mockServer.close());
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `onUnhandledRequest` defaults to `'error'` rather than msw's `'warn'`: in a
|
|
27
|
+
* test suite an unmocked request is a hole in the fixtures, and a warning that
|
|
28
|
+
* scrolls past is how a test ends up asserting against real network output.
|
|
29
|
+
*/
|
|
30
|
+
export function setupMockServer(mocks) {
|
|
31
|
+
const server = setupServer(...mocks.handlers);
|
|
32
|
+
return {
|
|
33
|
+
server,
|
|
34
|
+
listen: (overrides) => server.listen({ onUnhandledRequest: 'error', ...overrides }),
|
|
35
|
+
close: () => server.close(),
|
|
36
|
+
reset: () => {
|
|
37
|
+
mocks.resetState();
|
|
38
|
+
server.resetHandlers();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic randomness for generated fixtures. `Math.random` in a mock makes
|
|
3
|
+
* a failing test unreproducible and a visual snapshot unstable; a seeded
|
|
4
|
+
* generator gives varied-looking data that is identical on every run and on every
|
|
5
|
+
* machine.
|
|
6
|
+
*/
|
|
7
|
+
export type SeededRng = {
|
|
8
|
+
/** The next value in `[0, 1)`. */
|
|
9
|
+
next: () => number;
|
|
10
|
+
/**
|
|
11
|
+
* An integer in `[min, max]`, both inclusive. Bounds are trusted: `max` below
|
|
12
|
+
* `min` returns a value outside either, unchecked.
|
|
13
|
+
*/
|
|
14
|
+
int: (min: number, max: number) => number;
|
|
15
|
+
/** One element. Throws on an empty list. */
|
|
16
|
+
pick: <T>(items: readonly T[]) => T;
|
|
17
|
+
/** A shuffled copy; the input is left alone. */
|
|
18
|
+
shuffle: <T>(items: readonly T[]) => T[];
|
|
19
|
+
/** Rewinds to the seed. Pass this to `setupMocks({ onReset })`. */
|
|
20
|
+
reset: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A mulberry32 generator — 32 bits of state, a handful of integer ops, no
|
|
24
|
+
* dependency. Its statistical quality is irrelevant here; reproducibility and
|
|
25
|
+
* being cheap enough to call inside a request handler are the requirements.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSeededRng(seed: number): SeededRng;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic randomness for generated fixtures. `Math.random` in a mock makes
|
|
3
|
+
* a failing test unreproducible and a visual snapshot unstable; a seeded
|
|
4
|
+
* generator gives varied-looking data that is identical on every run and on every
|
|
5
|
+
* machine.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A mulberry32 generator — 32 bits of state, a handful of integer ops, no
|
|
9
|
+
* dependency. Its statistical quality is irrelevant here; reproducibility and
|
|
10
|
+
* being cheap enough to call inside a request handler are the requirements.
|
|
11
|
+
*/
|
|
12
|
+
export function createSeededRng(seed) {
|
|
13
|
+
let state = seed >>> 0;
|
|
14
|
+
const next = () => {
|
|
15
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
16
|
+
let t = state;
|
|
17
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
18
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
19
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
|
|
20
|
+
};
|
|
21
|
+
const int = (min, max) => min + Math.floor(next() * (max - min + 1));
|
|
22
|
+
return {
|
|
23
|
+
next,
|
|
24
|
+
int,
|
|
25
|
+
pick: (items) => {
|
|
26
|
+
// Guard on length, not on the read being `undefined`: `T` may itself
|
|
27
|
+
// include `undefined`, and `pick([undefined, 1])` must return the element
|
|
28
|
+
// rather than report an empty list.
|
|
29
|
+
if (items.length === 0) {
|
|
30
|
+
throw new Error('http-client-msw: cannot pick from an empty list');
|
|
31
|
+
}
|
|
32
|
+
return items[int(0, items.length - 1)];
|
|
33
|
+
},
|
|
34
|
+
shuffle: (items) => {
|
|
35
|
+
const shuffled = [...items];
|
|
36
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
37
|
+
const j = int(0, i);
|
|
38
|
+
// Both indices are in range by the loop bound, so the swap is
|
|
39
|
+
// unconditional — skipping it on an `undefined` *element* would let the
|
|
40
|
+
// result depend on the values while still consuming the draw above,
|
|
41
|
+
// which is exactly the reproducibility this generator exists to give.
|
|
42
|
+
const a = shuffled[i];
|
|
43
|
+
shuffled[i] = shuffled[j];
|
|
44
|
+
shuffled[j] = a;
|
|
45
|
+
}
|
|
46
|
+
return shuffled;
|
|
47
|
+
},
|
|
48
|
+
reset: () => {
|
|
49
|
+
state = seed >>> 0;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RequestHandler, WebSocketHandler } from 'msw';
|
|
2
|
+
/**
|
|
3
|
+
* Anything msw accepts as a handler. Typed handlers from `createMockApi` are
|
|
4
|
+
* `RequestHandler`s; the union leaves room for a websocket mock in the same
|
|
5
|
+
* array without a second setup path.
|
|
6
|
+
*/
|
|
7
|
+
export type MockSetupHandler = RequestHandler | WebSocketHandler;
|
|
8
|
+
/**
|
|
9
|
+
* A callback that returns mock state to its seeded starting point. Stores built
|
|
10
|
+
* with `createMockStore` expose exactly this as `store.reset`.
|
|
11
|
+
*/
|
|
12
|
+
export type MockResetCallback = () => void;
|
|
13
|
+
export type MockSetupOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Callbacks that restore mock state, run **in declaration order** by `reset()`
|
|
16
|
+
* on whichever entry serves these handlers. This is how a stateful mock stops
|
|
17
|
+
* leaking writes from one test into the next: the handlers close over a store,
|
|
18
|
+
* and the store's `reset` is declared here once rather than at every call site.
|
|
19
|
+
*
|
|
20
|
+
* Order matters when one reset feeds another. A store whose seed function draws
|
|
21
|
+
* from a seeded rng must be listed *after* that rng, or each reset re-seeds
|
|
22
|
+
* from wherever the previous one left the sequence.
|
|
23
|
+
*/
|
|
24
|
+
onReset?: readonly MockResetCallback[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* An environment-neutral description of a mock deployment: the handler array
|
|
28
|
+
* plus the state resets that belong with it. It holds no msw setup object of its
|
|
29
|
+
* own, which is what lets the same value be handed to the browser entry
|
|
30
|
+
* (`setupWorker`) or the node entry (`setupServer`) without either environment's
|
|
31
|
+
* msw import reaching the other's bundle.
|
|
32
|
+
*/
|
|
33
|
+
export type MockSetup = {
|
|
34
|
+
handlers: readonly MockSetupHandler[];
|
|
35
|
+
/** Runs every `onReset` callback in declaration order. */
|
|
36
|
+
resetState: () => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Bundles handlers with their state resets.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const rng = createSeededRng(42);
|
|
43
|
+
* const things = createMockStore(() => [
|
|
44
|
+
* { id: 't1', name: 'First', size: rng.int(1, 100) },
|
|
45
|
+
* ]);
|
|
46
|
+
*
|
|
47
|
+
* export const mocks = setupMocks(
|
|
48
|
+
* [mock.get('/things', ({ response }) => response(200).json(things.list()))],
|
|
49
|
+
* // The rng first: the store's seed draws from it.
|
|
50
|
+
* { onReset: [rng.reset, things.reset] },
|
|
51
|
+
* );
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function setupMocks(handlers: readonly MockSetupHandler[], options?: MockSetupOptions): MockSetup;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundles handlers with their state resets.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* const rng = createSeededRng(42);
|
|
6
|
+
* const things = createMockStore(() => [
|
|
7
|
+
* { id: 't1', name: 'First', size: rng.int(1, 100) },
|
|
8
|
+
* ]);
|
|
9
|
+
*
|
|
10
|
+
* export const mocks = setupMocks(
|
|
11
|
+
* [mock.get('/things', ({ response }) => response(200).json(things.list()))],
|
|
12
|
+
* // The rng first: the store's seed draws from it.
|
|
13
|
+
* { onReset: [rng.reset, things.reset] },
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function setupMocks(handlers, options = {}) {
|
|
18
|
+
const onReset = options.onReset ?? [];
|
|
19
|
+
return {
|
|
20
|
+
handlers,
|
|
21
|
+
resetState: () => {
|
|
22
|
+
for (const reset of onReset)
|
|
23
|
+
reset();
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { StartOptions } from 'msw/browser';
|
|
2
|
+
/**
|
|
3
|
+
* The browser entry's decisions, factored out of `browser.ts` so they are
|
|
4
|
+
* testable without a service worker: a vitest run cannot register one, but it
|
|
5
|
+
* can assert the options that would be passed and the start-once behaviour.
|
|
6
|
+
*/
|
|
7
|
+
export type MockWorkerOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The app's **public base path** — `import.meta.env.BASE_URL` under Vite — used
|
|
10
|
+
* to locate the worker script for a subpath deployment. This is not the API
|
|
11
|
+
* base from `createMockApi`: the script is served by the app, the API is
|
|
12
|
+
* whatever the app talks to.
|
|
13
|
+
*
|
|
14
|
+
* @default '/'
|
|
15
|
+
*/
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* What msw does with a request no handler matched.
|
|
19
|
+
*
|
|
20
|
+
* Defaults to `'bypass'`, unlike msw's own `'warn'`: an app running against
|
|
21
|
+
* mocks still loads its own documents, modules, images, and fonts, and warning
|
|
22
|
+
* on every one of them buries the requests a developer is actually looking at.
|
|
23
|
+
* Use `'warn'` while filling gaps in a mock set.
|
|
24
|
+
*/
|
|
25
|
+
onUnhandledRequest?: StartOptions['onUnhandledRequest'];
|
|
26
|
+
/**
|
|
27
|
+
* Suppress msw's per-request console logging. Defaults to `true` — the request
|
|
28
|
+
* log is long enough to hide application logs, and the network panel already
|
|
29
|
+
* shows the same traffic.
|
|
30
|
+
*/
|
|
31
|
+
quiet?: boolean;
|
|
32
|
+
/** Escape hatch merged last into msw's own `worker.start()` options. */
|
|
33
|
+
start?: StartOptions;
|
|
34
|
+
};
|
|
35
|
+
export declare function buildWorkerStartOptions(options?: MockWorkerOptions): StartOptions;
|
|
36
|
+
export type IdempotentStart = {
|
|
37
|
+
/** Runs the wrapped function at most once, until `invalidate()`. */
|
|
38
|
+
start: () => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Forgets a completed start so the next `start()` runs again. Whatever the
|
|
41
|
+
* start registered has to be torn down first — a memo pointing at a stopped
|
|
42
|
+
* worker resolves immediately and intercepts nothing.
|
|
43
|
+
*/
|
|
44
|
+
invalidate: () => void;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Wraps a start function so concurrent and repeated calls share one
|
|
48
|
+
* registration. An app entry, a hot reload, and a Playwright fixture can all
|
|
49
|
+
* call `start()`; registering the worker again mid-session would reset its
|
|
50
|
+
* handler list and drop any runtime overrides a test had installed.
|
|
51
|
+
*
|
|
52
|
+
* A rejected start invalidates itself, so a caller can retry after fixing
|
|
53
|
+
* whatever failed (a missing `mockServiceWorker.js`, most often).
|
|
54
|
+
*/
|
|
55
|
+
export declare function createIdempotentStart(start: () => Promise<void>): IdempotentStart;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolveWorkerScriptUrl } from './base-url.js';
|
|
2
|
+
export function buildWorkerStartOptions(options = {}) {
|
|
3
|
+
const { baseUrl, onUnhandledRequest, quiet, start } = options;
|
|
4
|
+
return {
|
|
5
|
+
...start,
|
|
6
|
+
// Resolved after the spread rather than defaulted before it: an options
|
|
7
|
+
// object forwarded with explicit `undefined` values would otherwise put
|
|
8
|
+
// those keys back and hand msw its own defaults instead of ours. `start`
|
|
9
|
+
// still wins over the named options where it sets a value.
|
|
10
|
+
onUnhandledRequest: start?.onUnhandledRequest ?? onUnhandledRequest ?? 'bypass',
|
|
11
|
+
quiet: start?.quiet ?? quiet ?? true,
|
|
12
|
+
serviceWorker: {
|
|
13
|
+
...start?.serviceWorker,
|
|
14
|
+
// The same hole one level down: a `serviceWorker` object populated from
|
|
15
|
+
// another options bag carries an explicit `url: undefined`, which spread
|
|
16
|
+
// over the resolved path would erase it and send msw to its own default
|
|
17
|
+
// location — the wrong one for a subpath deployment.
|
|
18
|
+
url: start?.serviceWorker?.url ?? resolveWorkerScriptUrl(baseUrl),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Wraps a start function so concurrent and repeated calls share one
|
|
24
|
+
* registration. An app entry, a hot reload, and a Playwright fixture can all
|
|
25
|
+
* call `start()`; registering the worker again mid-session would reset its
|
|
26
|
+
* handler list and drop any runtime overrides a test had installed.
|
|
27
|
+
*
|
|
28
|
+
* A rejected start invalidates itself, so a caller can retry after fixing
|
|
29
|
+
* whatever failed (a missing `mockServiceWorker.js`, most often).
|
|
30
|
+
*/
|
|
31
|
+
export function createIdempotentStart(start) {
|
|
32
|
+
let pending;
|
|
33
|
+
const invalidate = () => {
|
|
34
|
+
pending = undefined;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
start: () => {
|
|
38
|
+
// Called through an async wrapper so a synchronous throw from `start`
|
|
39
|
+
// rejects the returned promise instead of escaping as an exception the
|
|
40
|
+
// caller has no way to `.catch`.
|
|
41
|
+
pending ??= (async () => await start())().catch((error) => {
|
|
42
|
+
invalidate();
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
return pending;
|
|
46
|
+
},
|
|
47
|
+
invalidate,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@r0hitsharma/http-client-msw",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./browser": {
|
|
20
|
+
"types": "./dist/browser.d.ts",
|
|
21
|
+
"default": "./dist/browser.js"
|
|
22
|
+
},
|
|
23
|
+
"./node": {
|
|
24
|
+
"types": "./dist/node.d.ts",
|
|
25
|
+
"default": "./dist/node.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"clean": "rm -rf dist",
|
|
31
|
+
"prepare": "npm run build",
|
|
32
|
+
"type:check": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"lint": "oxlint -c oxlint.config.ts --max-warnings=0 src",
|
|
34
|
+
"lint:fix": "oxlint -c oxlint.config.ts --fix src",
|
|
35
|
+
"format": "oxfmt -c oxfmt.config.ts --write src",
|
|
36
|
+
"format:check": "oxfmt -c oxfmt.config.ts --check src",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"openapi-msw": "2.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"msw": "^2.10.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@r0hitsharma/oxfmt-config": "*",
|
|
47
|
+
"@r0hitsharma/oxlint-config": "*",
|
|
48
|
+
"@r0hitsharma/tsconfig": "*",
|
|
49
|
+
"@types/node": "24.13.3",
|
|
50
|
+
"msw": "2.15.0",
|
|
51
|
+
"oxfmt": "0.67.0",
|
|
52
|
+
"oxlint": "1.82.0",
|
|
53
|
+
"vitest": "^4.1.9"
|
|
54
|
+
},
|
|
55
|
+
"version": "0.12.0-rohit-fork-ci.1",
|
|
56
|
+
"repository": {
|
|
57
|
+
"url": "https://github.com/r0hitsharma/uikit"
|
|
58
|
+
}
|
|
59
|
+
}
|