Skip to content

Commit ad3964d

Browse files
authored
feat: Add MockPlugin (#3622)
1 parent 4453a57 commit ad3964d

File tree

15 files changed

+955
-31
lines changed

15 files changed

+955
-31
lines changed

.changeset/clever-houses-beam.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@data-client/core': patch
3+
---
4+
5+
Add @data-client/core/mock
6+
7+
New exports:
8+
- `MockController` - Controller wrapper for mocking endpoints
9+
- `collapseFixture` - Resolves fixture responses (handles function responses)
10+
- `createFixtureMap` - Separates fixtures into static map and interceptors
11+
- Types: `MockProps`, `Fixture`, `SuccessFixture`, `ErrorFixture`, `Interceptor`, `ResponseInterceptor`, `FetchInterceptor`, `FixtureEndpoint`, `SuccessFixtureEndpoint`, `ErrorFixtureEndpoint`

.changeset/gentle-heads-rule.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
'@data-client/vue': patch
3+
---
4+
5+
Add MockPlugin
6+
7+
Example usage:
8+
9+
```ts
10+
import { createApp } from 'vue';
11+
import { DataClientPlugin } from '@data-client/vue';
12+
import { MockPlugin } from '@data-client/vue/test';
13+
14+
const app = createApp(App);
15+
app.use(DataClientPlugin);
16+
app.use(MockPlugin, {
17+
fixtures: [
18+
{
19+
endpoint: MyResource.get,
20+
args: [{ id: 1 }],
21+
response: { id: 1, name: 'Test' },
22+
},
23+
],
24+
});
25+
app.mount('#app');
26+
```
27+
28+
Interceptors allow dynamic responses based on request arguments:
29+
30+
```ts
31+
app.use(MockPlugin, {
32+
fixtures: [
33+
{
34+
endpoint: MyResource.get,
35+
response: (...args) => {
36+
const [{ id }] = args;
37+
return {
38+
id,
39+
name: `Dynamic ${id}`,
40+
};
41+
},
42+
},
43+
],
44+
});
45+
```
46+
47+
Interceptors can also maintain state across calls:
48+
49+
```ts
50+
const interceptorData = { count: 0 };
51+
52+
app.use(MockPlugin, {
53+
fixtures: [
54+
{
55+
endpoint: MyResource.get,
56+
response: function (this: { count: number }, ...args) {
57+
this.count++;
58+
const [{ id }] = args;
59+
return {
60+
id,
61+
name: `Call ${this.count}`,
62+
};
63+
},
64+
},
65+
],
66+
getInitialInterceptorData: () => interceptorData,
67+
});
68+
```

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const baseConfig = {
2828
'packages/graphql',
2929
'packages/rest/src/next',
3030
'packages/core/src/next',
31+
'packages/core/src/mock',
3132
'packages/react/src/next',
3233
'packages/react/src/server',
3334
'packages/react/src/components/DevToolsButton.tsx',

packages/core/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"next": [
1717
"lib/next/index.d.ts"
1818
],
19+
"mock": [
20+
"lib/mock/index.d.ts"
21+
],
1922
"*": [
2023
"lib/index.d.ts"
2124
]
@@ -27,6 +30,9 @@
2730
"next": [
2831
"ts3.4/next/index.d.ts"
2932
],
33+
"mock": [
34+
"ts3.4/mock/index.d.ts"
35+
],
3036
"*": [
3137
"ts3.4/index.d.ts"
3238
]
@@ -48,6 +54,13 @@
4854
"react-native": "./lib/next/index.js",
4955
"default": "./lib/next/index.js"
5056
},
57+
"./mock": {
58+
"types": "./lib/mock/index.d.ts",
59+
"require": "./lib/mock/index.js",
60+
"browser": "./lib/mock/index.js",
61+
"react-native": "./lib/mock/index.js",
62+
"default": "./lib/mock/index.js"
63+
},
5164
"./package.json": "./package.json"
5265
},
5366
"type": "module",
Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,18 @@ import {
33
Controller,
44
DataClientDispatch,
55
GenericDispatch,
6-
} from '@data-client/core';
6+
} from '../index.js';
7+
import { collapseFixture } from './collapseFixture.js';
8+
import { createFixtureMap } from './createFixtureMap.js';
9+
import type { Fixture, Interceptor } from './fixtureTypes.js';
10+
import { MockProps } from './mockTypes.js';
711

8-
import { collapseFixture } from './collapseFixture';
9-
import { createFixtureMap } from './createFixtureMap';
10-
import type { Fixture, Interceptor } from './fixtureTypes';
11-
12-
export interface MockProps<T = any> {
13-
fixtures?: (Fixture | Interceptor<T>)[];
14-
getInitialInterceptorData?: () => T;
15-
}
16-
17-
export default function MockController<TBase extends typeof Controller, T>(
12+
export function MockController<TBase extends typeof Controller, T>(
1813
Base: TBase,
1914
{
2015
fixtures = [],
2116
getInitialInterceptorData = () => ({}) as any,
22-
}: MockProps<T> = {},
17+
}: MockProps<T>,
2318
): TBase {
2419
const [fixtureMap, interceptors] = createFixtureMap(fixtures);
2520

@@ -63,11 +58,11 @@ export default function MockController<TBase extends typeof Controller, T>(
6358
let fixture: Fixture | Interceptor | undefined;
6459
if (this.fixtureMap.has(key)) {
6560
fixture = this.fixtureMap.get(key) as Fixture;
66-
if (!args) args = (fixture as any).args;
61+
if (!args) args = fixture.args;
6762
// exact matches take priority; now test ComputedFixture
6863
} else {
6964
for (const cfix of this.interceptors) {
70-
if ((cfix.endpoint as any).testKey(key)) {
65+
if (cfix.endpoint.testKey(key)) {
7166
fixture = cfix;
7267
break;
7368
}
@@ -79,15 +74,15 @@ export default function MockController<TBase extends typeof Controller, T>(
7974
...action,
8075
};
8176
const delayMs =
82-
typeof (fixture as any).delay === 'function' ?
83-
(fixture as any).delay(...(args as any))
84-
: ((fixture as any).delay ?? 0);
77+
typeof fixture.delay === 'function' ?
78+
fixture.delay(...(args as any))
79+
: (fixture.delay ?? 0);
8580

8681
if ('fetchResponse' in fixture) {
87-
const { fetchResponse } = fixture as any;
82+
const { fetchResponse } = fixture;
8883
fixture = {
8984
endpoint: fixture.endpoint,
90-
response(...args: any[]) {
85+
response(...args) {
9186
const endpoint = (action.endpoint as any).extend({
9287
fetchResponse: (input: RequestInfo, init: RequestInit) => {
9388
const ret = fetchResponse.call(this, input, init);
@@ -103,23 +98,23 @@ export default function MockController<TBase extends typeof Controller, T>(
10398
});
10499
return (endpoint as any)(...args);
105100
},
106-
} as any;
101+
};
107102
}
108103
const fetch = async () => {
109104
if (!fixture) {
110105
throw new Error('No fixture found');
111106
}
112107
// delayCollapse determines when the fixture function is 'collapsed' (aka 'run')
113108
// collapsed: https://en.wikipedia.org/wiki/Copenhagen_interpretation
114-
if ((fixture as any).delayCollapse) {
109+
if (fixture.delayCollapse) {
115110
await new Promise(resolve => setTimeout(resolve, delayMs));
116111
}
117112
const result = await collapseFixture(
118113
fixture as any,
119114
args as any,
120115
this.interceptorData,
121116
);
122-
if (!(fixture as any).delayCollapse && delayMs) {
117+
if (!fixture.delayCollapse && delayMs) {
123118
await new Promise(resolve => setTimeout(resolve, delayMs));
124119
}
125120
if (result.error) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Fixture, ResponseInterceptor } from './fixtureTypes';
1+
import type { Fixture, ResponseInterceptor } from './fixtureTypes.js';
22

33
export async function collapseFixture(
44
fixture: Fixture | ResponseInterceptor,

packages/vue/src/test/createFixtureMap.ts renamed to packages/core/src/mock/createFixtureMap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Fixture, Interceptor } from './fixtureTypes';
1+
import type { Fixture, Interceptor } from './fixtureTypes.js';
22

33
export function createFixtureMap(fixtures: (Fixture | Interceptor)[] = []) {
44
const map: Map<string, Fixture> = new Map();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EndpointInterface, ResolveType } from '@data-client/core';
1+
import type { EndpointInterface, ResolveType } from '@data-client/endpoint';
22

33
type Updater = (
44
result: any,

packages/core/src/mock/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export { MockController } from './MockController.js';
2+
export type { MockProps } from './mockTypes.js';
3+
export type {
4+
Fixture,
5+
SuccessFixture,
6+
ErrorFixture,
7+
Interceptor,
8+
ResponseInterceptor,
9+
FetchInterceptor,
10+
FixtureEndpoint,
11+
SuccessFixtureEndpoint,
12+
ErrorFixtureEndpoint,
13+
} from './fixtureTypes.js';
14+
export { collapseFixture } from './collapseFixture.js';
15+
export { createFixtureMap } from './createFixtureMap.js';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Fixture, Interceptor } from './fixtureTypes.js';
2+
3+
export interface MockProps<T = any> {
4+
readonly fixtures?: (Fixture | Interceptor<T>)[];
5+
getInitialInterceptorData?: () => T;
6+
}

0 commit comments

Comments
 (0)