Next.js + MSW + Playwright を真面目に考えてみる
モチベーション
- E2E テストをしたい
- mutation 系もテストをしたいので mock を使いたい
- unit test で既に MSW を使っているので mock は MSW を使いたい
Next.js + Playwright + MSW の選択肢
一応 Next.js 側で next/experimental/testmode/playwright
と next/experimental/testmode/playwright/msw
という機能を export している
これは、playwright で next を実行したときに fetch に intercept してくれる Fixture を提供してくれる。
/msw
の方は fetch の intercept に msw の handler を使える
next/experimental/testmode/playwright/msw
現状だと msw v1 系にしか対応していないので msw v2 系 を利用している場合は使えない、残念
なので next/experimental/testmode/playwright
をベースに自分で msw v2 に対応した fixture を作る
type MswFixture = {
use: (...handlers: RequestHandler[]) => void
}
export const test = base.extend<{
msw: MswFixture
mswHandlers: RequestHandler[]
}>({
mswHandlers: [handlers, { option: true }],
msw: [
async ({ next, mswHandlers }, use) => {
const handlers: RequestHandler[] = [...mswHandlers]
const emitter = new Emitter<LifeCycleEventsMap>()
next.onFetch(async (request) => {
let isPassthrough = false
let mockedResponse: Response | null = null
await handleRequest(
request,
Math.random().toString(16).slice(2),
handlers,
{
onUnhandledRequest: () => {
isPassthrough = true
},
},
emitter,
{
onPassthroughResponse: () => {
isPassthrough = true
},
onMockedResponse: (r) => {
mockedResponse = r
},
},
)
if (isPassthrough) {
return 'continue'
}
if (mockedResponse) {
return mockedResponse
}
return 'abort'
})
await use({
use: (...newHandlers) => {
handlers.unshift(...newHandlers)
},
})
handlers.length = 0
},
{ auto: true },
],
})
msw v2 は Request/Response に対応したので msw v1 の fixture でやってる Request の詰め替えとかはやらなくても大丈夫になった
本家の v1 の fixture とやや違うのは UnhandledRequest を Passthrough と同じ挙動にしていること、Unhandled で止めてしまうと外部アクセス(Firebase Auth とか)で abort してしまうのでこのような対応にした
msw の handleRequest
自体が公式にドキュメントがないのでアレだが、コード上から msw を使うときに使えるっぽい
UnhandledRequest を Passthrough を continue
にしていたが、なんかうまくいかなかったので自分で fetch をしてバイパスすることにした。
onFetch
の abort
とか continue
とか undefined | null
の挙動ってどこかに書いてるのかな...
type MswFixture = {
use: (...handlers: RequestHandler[]) => void
}
export const test = base.extend<{
msw: MswFixture
mswHandlers: RequestHandler[]
}>({
mswHandlers: [handlers, { option: true }],
msw: [
async ({ next, mswHandlers }, use) => {
const handlers: RequestHandler[] = [...mswHandlers]
const emitter = new Emitter<LifeCycleEventsMap>()
next.onFetch(async (request) => {
let isPassthrough = false
let mockedResponse: Response | null = null
await handleRequest(
request.clone(),
Math.random().toString(16).slice(2),
handlers,
{
onUnhandledRequest: () => {
isPassthrough = true
},
},
emitter,
{
onPassthroughResponse: () => {
isPassthrough = true
},
onMockedResponse: (r) => {
mockedResponse = r
},
},
)
if (isPassthrough) {
return fetch(request.clone())
}
if (mockedResponse) {
return mockedResponse
}
return 'abort'
})
await use({
use: (...newHandlers) => {
handlers.unshift(...newHandlers)
},
})
handlers.length = 0
},
{ auto: true },
],
})
追加でちょっと詰まった部分
playwright では script 部分の実行が CJS を解釈するようになっているので ESM の場合はうまく読み込まれないよう
ここで言う script
というのは、今回で言う MSW の handler とか test(..., () => {...})
を書く部分。つまり、playwright の worker や test runner が実行するコードのこと。
今回は、lodash-es
を使っている部分でこのエラーが出てしばらく詰まってた(zip
だけ使いたかったので仕方なく lodash.zip
を入れて対応した)
少し詰まった部分
test.beforeEach(async ({ page }) => {
await page.goto('/xxx')
})
test('hoge', async ({ page, msw }) => {
msw.use(...)
await page...
})
のように書いたときに、hoge
の msw handler の設定がされていなかった
それはそう、という感じだが page.goto
が終わってから msw.use
になるのでページ描画に必要なAPIの設定がされないままページが描画されるという形だった
この場合
test('hoge', async ({ page, msw }) => {
msw.use(...)
await page.goto('/xxx')
await page...
})
としないといけない