Skip to content

Commit d81b46d

Browse files
committed
WIP: Basic plumbing for record/replay modes
1 parent 7dc00d1 commit d81b46d

File tree

5 files changed

+84
-13
lines changed

5 files changed

+84
-13
lines changed

example/index.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const App = () => {
2525
<ChakraProvider>
2626
<MSWToolbar
2727
worker={worker}
28-
apiUrl="http://www.example.com"
28+
apiUrl="https://pokeapi.co"
2929
isEnabled={true}
3030
actions={
3131
<HStack spacing={2}>
@@ -37,14 +37,16 @@ const App = () => {
3737
>
3838
<Button
3939
onClick={() =>
40-
fetch('http://www.example.com').then(async (res) => {
41-
if (res.ok) {
42-
const content = await res.json();
43-
alert(
44-
`Here is the mocked response!: ${JSON.stringify(content)}`
45-
);
40+
fetch('https://pokeapi.co/api/v2/pokemon/ditto').then(
41+
async (res) => {
42+
if (res.ok) {
43+
const content = await res.json();
44+
alert(
45+
`Here is the mocked response!: ${JSON.stringify(content)}`
46+
);
47+
}
4648
}
47-
})
49+
)
4850
}
4951
mt={50}
5052
>

src/component/MSWToolbar.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { rest } from 'msw';
2+
import { RequestHandler, rest } from 'msw';
33
import {
44
SetupWorkerApi,
55
MockedRequest,
@@ -10,7 +10,35 @@ import { usePrevious } from './hooks';
1010
import styles from './styles.module.css';
1111
import { WorkerMode } from '../types';
1212
import { get, modes, set } from '../helpers';
13-
import { MSWToolbarProps } from '..';
13+
import { MSWToolbarProps, TrackedRequest } from '..';
14+
15+
async function buildHandlersFromRecordedRequests(
16+
trackedRequests: TrackedRequest
17+
): Promise<RequestHandler[]> {
18+
let handlers = [];
19+
for (const [_reqId, { request, response }] of trackedRequests.entries()) {
20+
if (!response) continue;
21+
22+
// TODO: this should probably happen before we add it to the map,
23+
// and we'd most likely want to set the response type there (text, json)
24+
// along with the data
25+
const data = await response.json();
26+
27+
const handler = (rest as any)[request.method.toLocaleLowerCase()](
28+
request.url.href,
29+
(
30+
_req: MockedRequest<any>,
31+
res: ResponseComposition<any>,
32+
ctx: RestContext
33+
) => {
34+
return res(ctx.json(data));
35+
}
36+
);
37+
handlers.push(handler);
38+
}
39+
40+
return handlers;
41+
}
1442

1543
/**
1644
* This is a simple toolbar that allows you to toggle MSW handlers in local development as well as easily invalidate all of the caches.
@@ -35,6 +63,7 @@ export const MSWToolbar = ({
3563
}
3664

3765
const workerRef = React.useRef<SetupWorkerApi>();
66+
const trackedRequestsRef = React.useRef<TrackedRequest>(new Map());
3867

3968
const [isReady, setIsReady] = React.useState(isEnabled ? false : true);
4069

@@ -83,7 +112,39 @@ export const MSWToolbar = ({
83112
set(prefix, 'mode', mode);
84113

85114
switch (mode) {
115+
case 'record':
116+
// Passing empty handlers will cause everything to run as a bypass
117+
workerRef.current?.use(...[]);
118+
workerRef.current?.events.on('request:start', request => {
119+
trackedRequestsRef.current.set(request.id, {
120+
request,
121+
response: null as any,
122+
});
123+
});
124+
125+
workerRef.current?.events.on('response:bypass', async (res, reqId) => {
126+
const clone = res.clone();
127+
const existingRequestEntry = trackedRequestsRef.current.get(reqId);
128+
if (existingRequestEntry) {
129+
trackedRequestsRef.current.set(reqId, {
130+
...existingRequestEntry,
131+
response: clone,
132+
});
133+
}
134+
});
135+
136+
return;
137+
case 'replay':
138+
workerRef.current?.events.removeAllListeners();
139+
buildHandlersFromRecordedRequests(trackedRequestsRef.current).then(
140+
handlers => {
141+
workerRef.current?.resetHandlers(...handlers);
142+
}
143+
);
144+
145+
return;
86146
case 'normal':
147+
workerRef.current?.events.removeAllListeners();
87148
workerRef.current?.resetHandlers();
88149
return;
89150
case 'error':

src/helpers/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Setting } from '..';
22
import { WorkerMode, WorkerStatus } from '../types';
33

4-
export const modes: WorkerMode[] = ['normal', 'error'];
4+
export const modes: WorkerMode[] = ['normal', 'error', 'record', 'replay'];
55

66
type SettingValueMap = {
77
mode: WorkerMode;

src/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SetupWorkerApi } from 'msw';
1+
import { MockedRequest, SetupWorkerApi } from 'msw';
22

33
export interface MSWToolbarProps {
44
/**
@@ -32,4 +32,11 @@ export interface MSWToolbarProps {
3232

3333
export type Setting = 'mode' | 'delay' | 'status';
3434
export type WorkerStatus = 'enabled' | 'disabled';
35-
export type WorkerMode = 'normal' | 'error';
35+
export type WorkerMode = 'normal' | 'error' | 'record' | 'replay';
36+
export type TrackedRequest = Map<
37+
string,
38+
{
39+
request: MockedRequest;
40+
response: Response | null;
41+
}
42+
>;

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"forceConsistentCasingInFileNames": true,
3232
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
3333
"noEmit": true,
34+
"target": "es2015"
3435
}
3536
}

0 commit comments

Comments
 (0)