-
-
Notifications
You must be signed in to change notification settings - Fork 535
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
41ffaa0
commit 61af97f
Showing
1 changed file
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,390 @@ | ||
# Migration guide | ||
|
||
## Response resolver | ||
|
||
A response resolver now exposes a single object argument instead of `(req, res, ctx)`. That argument represents resolver information and consists of properties that are always present for all handler types and extra properties specific to handler types. | ||
|
||
### Resolver info | ||
|
||
#### General | ||
|
||
- `request`, a Fetch API `Request` instance representing a captured request. | ||
- `cookies`, a parsed cookies object based on the request cookies. | ||
|
||
#### REST-specific | ||
|
||
- `params`, an object of parsed path parameters. | ||
|
||
#### GraphQL-specific | ||
|
||
- `query`, a GraphQL query string extracted from either URL search parameters or a POST request body. | ||
- `variables`, an object of GraphQL query variables. | ||
|
||
### Using a new signature | ||
|
||
To mock responses, you should now return a Fetch API `Response` instance from the response resolver. You no longer need to compose a response via `res()`, and all the context utilities have also [been removed](#context-utilities). | ||
|
||
```js | ||
rest.get('/greet/:name', ({ request, params }) => { | ||
console.log('Captured %s %s', request.method, request.url) | ||
return new Response(`hello, ${params.name}!`) | ||
}) | ||
``` | ||
|
||
Now, a more complex example for both REST and GraphQL requests. | ||
|
||
```js | ||
import { rest, graphql } from 'msw' | ||
|
||
export const handlers = [ | ||
rest.put('/user/:id', async ({ request, params, cookies }) => { | ||
// Read request body as you'd normally do with Fetch. | ||
const payload = await request.json() | ||
// Access path parameters like before. | ||
const { id } = params | ||
// Access cookies like before. | ||
const { sessionId } = cookies | ||
|
||
return new Response(null, { status: 201 }) | ||
}), | ||
|
||
graphql.mutation('CreateUser', ({ request, query, variables }) => { | ||
return new Response( | ||
JSON.stringify({ | ||
data: { | ||
user: { | ||
id: 'abc-123', | ||
firstName: variables.firstName, | ||
}, | ||
}, | ||
}), | ||
{ | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}, | ||
) | ||
}), | ||
] | ||
``` | ||
|
||
### Request changes | ||
|
||
Since the returned `request` is now an instance of Fetch API `Request`, there are some changes to its properties. | ||
|
||
#### Request URL | ||
|
||
The `request.url` property is a string (previously, a `URL` instance). If you wish to operate with it like a `URL`, you need to construct it manually: | ||
|
||
```js | ||
rest.get('/prodct', ({ request }) => { | ||
// For example, this is how you would access | ||
// request search parameters now. | ||
const url = new URL(request.url) | ||
const productId = url.searchParams.get('id') | ||
}) | ||
``` | ||
|
||
#### Path parameters | ||
|
||
Path parameters are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.params`). | ||
|
||
#### Request cookies | ||
|
||
Request cookies are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.cookies`). | ||
|
||
#### Request body | ||
|
||
The library now does no assumptions when reading the intercepted request's body (previously, `req.body`). Instead, you are in charge to read the request body as you see appropriate. | ||
|
||
> Note that since the intercepted request is now represented by a Fetch API `Request` instance, its `request.body` property still exists but returns a `ReadableStream`. | ||
For example, this is how you would read request body: | ||
|
||
```js | ||
rest.post('/user', async ({ request }) => { | ||
const nextUser = await request.json() | ||
// request.formData() / request.arrayBuffer() / etc. | ||
}) | ||
``` | ||
|
||
### Convenient response declarations | ||
|
||
Using the Fetch API `Response` instance may get quite verbose. To give you more convenient means of declaring mocked responses while remaining specification compliant and compatible, the library now exports an `HttpResponse` object. You can use that object to construct response instances faster. | ||
|
||
```js | ||
import { rest, HttpResponse } from 'msw' | ||
|
||
export const handlers = [ | ||
rest.get('/user', () => { | ||
// This is synonymous to "ctx.json()": | ||
// HttpResponse.json() stringifies the given body | ||
// and sets the correct "Content-Type" response header | ||
// to describe a JSON response body. | ||
return HttpResponse.json({ firstName: 'John' }) | ||
}), | ||
] | ||
``` | ||
|
||
> Read more on how to use `HttpResponse` to mock [REST API](#rest-response-body-utilities) and [GraphQL API](#graphql-response-body-utilities) responses. | ||
## Responses in Node.js | ||
|
||
Although MSW now respects the Fetch API specification, the older versions of Node.js do not, so you can't construct a `Response` instance because there is no such global class. | ||
|
||
To account for this, the library exports a `Response` class that you should use when declaring request handlers. Behind the hood, that response class is resolved to a compatible polyfill in Node.js; in the browser, it only aliases `global.Response` without introducing additional behaviors. | ||
|
||
```js | ||
import { rest, Response } from 'msw' | ||
|
||
setupServer( | ||
rest.get('/ping', () => { | ||
return new Response('hello world) | ||
}) | ||
) | ||
``` | ||
Relying on a single universal `Response` class will allow you to write request handlers that can run in both browser and Node.js environments. | ||
## One-time responses | ||
To create a one-time request handler, pass it an object as the third argument with `once: true` set: | ||
```js | ||
import { HttpResponse, rest } from 'msw' | ||
export const handlers = [ | ||
rest.get('/user', () => HttpResponse.text('hello'), { once: true }), | ||
] | ||
``` | ||
## Passthrough responses | ||
```js | ||
import { passthrough } from 'msw' | ||
export const handlers = [ | ||
rest.get('/user', () => { | ||
// Previously, "req.passthrough()". | ||
return passthrough() | ||
}), | ||
] | ||
``` | ||
--- | ||
## Context utilities | ||
Most of the context utilities you'd normally use via `ctx.*` were removed. Instead, we encourage you to set respective properties directly on the response instance: | ||
|
||
```js | ||
import { HttpResponse, rest } from 'msw' | ||
export const handlers = [ | ||
rest.post('/user', () => { | ||
// ctx.json() | ||
return HttpResponse.json( | ||
{ firstName: 'John' }, | ||
{ | ||
status: 201, // ctx.status() | ||
headers: { | ||
'X-Custom-Header': 'value', // ctx.set() | ||
}, | ||
}, | ||
) | ||
}), | ||
] | ||
``` | ||
|
||
### REST response body utilities | ||
|
||
All response body utilities, like `ctx.body()`, `ctx.text()`, `ctx.json()`, etc., were removed in favor of constructing a correct `Response` instance. However, since `Response` declarations may get verbose, the library now exports a `HttpResponse` abstraction to help you construct mocked responses with different body types easier. | ||
|
||
```js | ||
import { HttpResponse, rest } from 'msw' | ||
export const handlers = [ | ||
rest.get('/body', () => { | ||
// You can construct mocked responses with | ||
// arbitrary bodies via a direct Response instance. | ||
return new Response('raw-body', { | ||
headers: { | ||
'Content-Type': 'application/vnd.acme+json', | ||
}, | ||
}) | ||
}), | ||
rest.get('/text', () => { | ||
return HttpResponse.text('hello world') | ||
}), | ||
rest.get('/json', () => { | ||
return HttpResponse.json({ firstName: 'John' }) | ||
}), | ||
rest.get('/xml', () => { | ||
return HttpResponse.xml({ firstName: 'John' }) | ||
}), | ||
] | ||
``` | ||
|
||
> In addition, you can now mock other response bodies like `formData` or `blob` by accessing the respective methods on the `HttpResponse` object. | ||
|
||
### GraphQL response body utilities | ||
|
||
GraphQL context utilities have been removed in favor of constructing a correct JSON response instance. | ||
|
||
```js | ||
import { HttpResponse, graphql } from 'msw' | ||
export const handlers = [ | ||
graphql.query('GetUser', ({ variables }) => { | ||
return HttpResponse.json({ | ||
data: { | ||
// ctx.data() | ||
user: { | ||
id: variables.id, | ||
firstName: 'John', | ||
}, | ||
}, | ||
// ctx.errors() | ||
errors: [ | ||
{ | ||
message: 'Failed to fetch "user.posts"', | ||
}, | ||
], | ||
extensions: { | ||
// ctx.extensions() | ||
server: 'HTTP1.1 Apache', | ||
}, | ||
}) | ||
}), | ||
] | ||
``` | ||
|
||
### `ctx.delay()` | ||
|
||
You can delay a mocked response by awaiting the `delay()` function: | ||
|
||
```js | ||
import { rest, delay } from 'msw' | ||
export const handlers = [ | ||
rest.post('/user', async () => { | ||
await delay() | ||
}), | ||
] | ||
``` | ||
|
||
> The `delay()` function has the same call signature as the `ctx.delay()` used to have. | ||
|
||
### `ctx.fetch()` | ||
|
||
The `ctx.fetch()` function has been removed in favor of the `bypass()` function. You should now always perform a regular `fetch()` call and wrap the request in the `bypass()` function if you wish for it to ignore any otherwise matching request handlers. | ||
|
||
```js | ||
import { rest, bypass } from 'msw' | ||
|
||
export const handlers = [ | ||
rest.get('https://api.github.com/user/:username', async ({ request }) => { | ||
// Performs an original "GET" request to the GitHub REST API. | ||
const original = await fetch(bypass(request)) | ||
}), | ||
] | ||
``` | ||
### `ctx.cookie()` | ||
Please set the "Set-Cookie" response header in order to mock response cookies. | ||
```js | ||
import { HttpResponse, rest } from 'msw' | ||
|
||
export const handlers = [ | ||
rest.post('/login', () => { | ||
return HttpResponse.text(null, { | ||
headers: { | ||
'Set-Cookie': 'sessionId=abc123', | ||
}, | ||
}) | ||
}), | ||
] | ||
``` | ||
Since Fetch API Headers do not support multiple values as the `HeadersInit`, to mock a multi-value response cookie create a `Headers` instance and use the `.append()` method to set multiple `Set-Cookie` response headers. | ||
```js | ||
import { Headers, HttpResponse, rest } from 'msw' | ||
|
||
rest.post('/login', () => { | ||
const headers = new Headers() | ||
headers.append('Set-Cookie', 'sessionId=123') | ||
headers.append('Set-Cookie', 'gtm=en_US') | ||
|
||
return HttpResponse.plain(null, { headers }) | ||
}) | ||
``` | ||
## Life-cycle events | ||
The request and response instances exposed in the life-cycle API have also been updated to return Fetch API `Request` and `Response` respectively. | ||
The request ID is now exposed as a standalone argument (previously, `req.id`). | ||
```js | ||
server.events.on('request:start', (request, requestId) => { | ||
console.log(request.method, request.url) | ||
}) | ||
``` | ||
To read a request body, make sure to clone the request first. Otherwise, it won't be performed as it would be already read. | ||
```js | ||
server.events.on('request:match', async (request) => { | ||
// Make sure to clone the request so it could be | ||
// processed further down the line. | ||
const clone = request.clone() | ||
const json = await clone.json() | ||
|
||
console.log('Performed request with body:', json) | ||
}) | ||
``` | ||
--- | ||
## Advanced | ||
It is still possible to create custom handlers and resolvers, just make sure to account for the new [resolver call signature](#response-resolver). | ||
### Custom response composition | ||
As this release removes the concept of response composition via `res()`, you can no longer compose context utilities or abstract their partial composed state to a helper function. | ||
Instead, you can abstract a common response logic into a plain function and always returns a `Response` instance. | ||
```js | ||
// utils.js | ||
import { Response } from 'msw' | ||
|
||
export function augmentResponse() { | ||
const response = new Resopnse() | ||
response.headers.set('X-Response-Source', 'mocks') | ||
return response | ||
} | ||
``` | ||
```js | ||
import { rest, HttpResponse } from 'msw' | ||
import { augmentResponse } from './utils' | ||
|
||
export const handlers = [ | ||
rest.get('/user', () => { | ||
return augmentResponse(HttpResponse.json({ id: 1 })) | ||
}), | ||
] | ||
``` | ||
--- | ||
## Common issues | ||
### `Response is not defined` | ||
Make sure to import the `Response` class from the `msw` package. See [this](#responses-in-nodejs). |