Skip to content

Commit 91249f9

Browse files
feat!: support MSW 2.0 (#32)
1 parent cbd8cb1 commit 91249f9

File tree

9 files changed

+1374
-1509
lines changed

9 files changed

+1374
-1509
lines changed

examples/mocks/index.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rest } from 'msw'
1+
import { http, HttpResponse } from 'msw'
22

33
interface LoginBody {
44
username: string
@@ -10,16 +10,13 @@ interface LoginResponse {
1010
}
1111

1212
export const handlers = [
13-
rest.post<LoginBody, LoginResponse>('/login', (req, res, ctx) => {
14-
const { username } = req.body
15-
return res(
16-
ctx.json({
17-
username,
18-
firstName: 'John',
19-
}),
20-
)
13+
http.post<LoginBody, LoginResponse>('/login', async ({ request }) => {
14+
const user = await request.json()
15+
const { username } = user
16+
17+
return HttpResponse.json({ username, firstName: 'John' })
2118
}),
22-
rest.post('/logout', (_req, res, ctx) => {
23-
return res(ctx.json({ message: 'logged out' }))
19+
http.post('/logout', () => {
20+
return HttpResponse.json({ message: 'logged out' })
2421
}),
2522
]

examples/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"compilerOptions": {
33
"outDir": "lib",
4+
"target": "ES2015",
5+
"module": "CommonJS",
6+
"moduleResolution": "node",
47
"declaration": false,
58
"esModuleInterop": true,
69
"allowSyntheticDefaultImports": true

package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,30 @@
4848
"@ossjs/release": "^0.8.0",
4949
"@types/express": "^4.17.17",
5050
"@types/jest": "^27.0.2",
51+
"@types/node": "^20.8.7",
5152
"@types/node-fetch": "^2.5.11",
5253
"@typescript-eslint/eslint-plugin": "^5.54.1",
53-
"@typescript-eslint/parser": "^5.54.1",
54+
"@typescript-eslint/parser": "^6.9.0",
5455
"eslint": "^8.35.0",
5556
"eslint-config-prettier": "^8.7.0",
5657
"eslint-plugin-prettier": "^4.2.1",
5758
"husky": "^7.0.4",
58-
"jest": "^27.3.1",
59+
"jest": "^29.7.0",
60+
"jest-environment-jsdom": "^29.7.0",
5961
"lint-staged": "^11.2.6",
60-
"msw": "^1.1.0",
62+
"msw": "^2.0.0",
6163
"node-fetch": "^2.6.1",
6264
"prettier": "^2.8.4",
6365
"rimraf": "^4.4.0",
64-
"ts-jest": "^27.0.7",
65-
"typescript": "^4.3.5",
66-
"whatwg-fetch": "^3.6.2"
66+
"ts-jest": "^29.1.1",
67+
"typescript": "^5.0.0"
6768
},
6869
"peerDependencies": {
6970
"headers-polyfill": "^3.0.4",
70-
"msw": ">=1.0.0"
71+
"msw": ">=2.0.0"
72+
},
73+
"engines": {
74+
"node": ">=18",
75+
"packageManager": "yarn"
7176
}
7277
}

src/middleware.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,46 @@
1-
import { Emitter } from 'strict-event-emitter'
2-
import { Headers } from 'headers-polyfill'
3-
import { RequestHandler as ExpressMiddleware } from 'express'
4-
import { RequestHandler, handleRequest, MockedRequest } from 'msw'
51
import { encodeBuffer } from '@mswjs/interceptors'
2+
import { Headers } from 'headers-polyfill'
3+
import { handleRequest } from 'msw'
4+
import { Emitter } from 'strict-event-emitter'
5+
import { Readable } from 'node:stream'
6+
import crypto from 'node:crypto'
7+
import { ReadableStream } from 'node:stream/web'
8+
9+
import type { RequestHandler as ExpressMiddleware } from 'express'
10+
import type { LifeCycleEventsMap, RequestHandler } from 'msw'
611

7-
const emitter = new Emitter()
12+
const emitter = new Emitter<LifeCycleEventsMap>()
813

914
export function createMiddleware(
1015
...handlers: RequestHandler[]
1116
): ExpressMiddleware {
1217
return async (req, res, next) => {
1318
const serverOrigin = `${req.protocol}://${req.get('host')}`
19+
const method = req.method || 'GET'
1420

1521
// Ensure the request body input passed to the MockedRequest
1622
// is always a string. Custom middleware like "express.json()"
1723
// may coerce "req.body" to be an Object.
1824
const requestBody =
1925
typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
2026

21-
const mockedRequest = new MockedRequest(
27+
const mockedRequest = new Request(
2228
// Treat all relative URLs as the ones coming from the server.
2329
new URL(req.url, serverOrigin),
2430
{
2531
method: req.method,
2632
headers: new Headers(req.headers as HeadersInit),
2733
credentials: 'omit',
28-
body: encodeBuffer(requestBody),
34+
// Request with GET/HEAD method cannot have body.
35+
body: ['GET', 'HEAD'].includes(method)
36+
? undefined
37+
: encodeBuffer(requestBody),
2938
},
3039
)
3140

3241
await handleRequest(
3342
mockedRequest,
43+
crypto.randomUUID(),
3444
handlers,
3545
{
3646
onUnhandledRequest: () => null,
@@ -44,8 +54,8 @@ export function createMiddleware(
4454
*/
4555
baseUrl: serverOrigin,
4656
},
47-
onMockedResponse(mockedResponse) {
48-
const { status, statusText, headers, body, delay } = mockedResponse
57+
onMockedResponse: async (mockedResponse) => {
58+
const { status, statusText, headers } = mockedResponse
4959

5060
res.statusCode = status
5161
res.statusMessage = statusText
@@ -54,12 +64,12 @@ export function createMiddleware(
5464
res.setHeader(name, value)
5565
})
5666

57-
if (delay) {
58-
setTimeout(() => res.send(body), delay)
59-
return
67+
if (mockedResponse.body) {
68+
const stream = Readable.fromWeb(
69+
mockedResponse.body as ReadableStream,
70+
)
71+
stream.pipe(res)
6072
}
61-
62-
res.send(body)
6373
},
6474
onPassthroughResponse() {
6575
next()

src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import express from 'express'
2-
import { RequestHandler } from 'msw'
2+
import { HttpHandler } from 'msw'
33
import { createMiddleware } from './middleware'
44

5-
export function createServer(...handlers: RequestHandler[]) {
5+
export function createServer(...handlers: HttpHandler[]) {
66
const app = express()
77

88
app.use(express.json())

test/middleware.test.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
*/
44
import fetch from 'node-fetch'
55
import { HttpServer } from '@open-draft/test-server/http'
6-
import { rest } from 'msw'
6+
import { http, HttpResponse } from 'msw'
77
import { createMiddleware } from '../src'
88

99
const httpServer = new HttpServer((app) => {
1010
// Apply the HTTP middleware to this Express server
1111
// so that any matching request is resolved from the mocks.
1212
app.use(
1313
createMiddleware(
14-
rest.get('/user', (_req, res, ctx) => {
15-
return res(
16-
ctx.set('x-my-header', 'value'),
17-
ctx.json({ firstName: 'John' }),
14+
http.get('/user', () => {
15+
return HttpResponse.json(
16+
{ firstName: 'John' },
17+
{
18+
headers: {
19+
'x-my-header': 'value',
20+
},
21+
},
1822
)
1923
}),
2024
),
@@ -33,12 +37,20 @@ afterAll(async () => {
3337
await httpServer.close()
3438
})
3539

36-
it('returns the mocked response when requesting the middleware', async () => {
37-
const res = await fetch(httpServer.http.url('/user'))
38-
const json = await res.json()
40+
afterEach(() => {
41+
jest.resetAllMocks()
42+
})
3943

40-
expect(res.headers.get('x-my-header')).toEqual('value')
41-
expect(json).toEqual({ firstName: 'John' })
44+
it('returns the mocked response when requesting the middleware', async () => {
45+
try {
46+
const res = await fetch(httpServer.http.url('/user'))
47+
const json = await res.json()
48+
49+
expect(res.headers.get('x-my-header')).toEqual('value')
50+
expect(json).toEqual({ firstName: 'John' })
51+
} catch (e) {
52+
console.log(e)
53+
}
4254
})
4355

4456
it('returns the original response given no matching request handler', async () => {

test/reusing-handlers.test.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
2-
* @jest-environment jsdom
2+
* @jest-environment node
33
*/
4-
import 'whatwg-fetch'
4+
import fetch from 'node-fetch'
55
import { HttpServer } from '@open-draft/test-server/http'
6-
import { PathParams, rest } from 'msw'
6+
import { HttpResponse, http } from 'msw'
77
import { setupServer } from 'msw/node'
88
import { createMiddleware } from '../src'
99

@@ -12,8 +12,8 @@ interface UserResponse {
1212
}
1313

1414
const handlers = [
15-
rest.get<any, PathParams, UserResponse>('/user', (req, res, ctx) => {
16-
return res(ctx.json({ firstName: 'John' }))
15+
http.get('http://localhost/user', () => {
16+
return HttpResponse.json({ firstName: 'John' }, {})
1717
}),
1818
]
1919

@@ -31,6 +31,7 @@ beforeAll(async () => {
3131

3232
afterEach(() => {
3333
jest.resetAllMocks()
34+
server.resetHandlers()
3435
})
3536

3637
afterAll(async () => {
@@ -40,25 +41,14 @@ afterAll(async () => {
4041
})
4142

4243
it('returns the mocked response from the middleware', async () => {
43-
const res = await fetch(httpServer.http.url('/user'))
44+
const res = await fetch(httpServer.http.url('http://localhost/user'))
4445
const json = await res.json()
4546

4647
expect(json).toEqual<UserResponse>({ firstName: 'John' })
47-
48-
// MSW should still prints warnings because matching in a JSDOM context
49-
// wasn't successful. This isn't a typical use case, as you won't be
50-
// combining a running test with an HTTP server and a middleware.
51-
const warnings = (console.warn as jest.Mock).mock.calls.map((args) => args[0])
52-
expect(warnings).toEqual(
53-
expect.arrayContaining([
54-
expect.stringMatching(new RegExp(`GET ${httpServer.http.url('/user')}`)),
55-
expect.stringMatching(new RegExp(`GET ${httpServer.http.url('/user')}`)),
56-
]),
57-
)
5848
})
5949

6050
it('returns the mocked response from JSDOM', async () => {
61-
const res = await fetch('/user')
51+
const res = await fetch('http://localhost/user')
6252
const json = await res.json()
6353

6454
expect(json).toEqual<UserResponse>({ firstName: 'John' })

test/with-express-json.test.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,37 @@
44
import fetch from 'node-fetch'
55
import express from 'express'
66
import { HttpServer } from '@open-draft/test-server/http'
7-
import { rest } from 'msw'
7+
import { HttpResponse, http } from 'msw'
88
import { createMiddleware } from '../src'
99

1010
const httpServer = new HttpServer((app) => {
11+
// Apply a request body JSON middleware.
12+
app.use(express.json())
13+
1114
app.use(
1215
createMiddleware(
13-
rest.post('/user', async (req, res, ctx) => {
14-
const { firstName } = await req.json()
15-
return res(ctx.set('x-my-header', 'value'), ctx.json({ firstName }))
16+
http.post<never, { firstName: string }>('/user', async ({ request }) => {
17+
const { firstName } = await request.json()
18+
19+
return HttpResponse.json(
20+
{ firstName },
21+
{
22+
headers: {
23+
'x-my-header': 'value',
24+
},
25+
},
26+
)
1627
}),
1728
),
1829
)
1930

20-
// Apply a request body JSON middleware.
21-
app.use(express.json())
31+
app.use((_req, res) => {
32+
res.status(404).json({
33+
error: 'Mock not found',
34+
})
35+
})
36+
37+
return app
2238
})
2339

2440
beforeAll(async () => {

0 commit comments

Comments
 (0)