Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concept: test mode for Playwright and similar integration tools #52520

Merged
merged 6 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/playwright.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../../dist/experimental/testmode/playwright'
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/playwright.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../dist/experimental/testmode/playwright')
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/playwright/msw.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../../../dist/experimental/testmode/playwright/msw'
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/playwright/msw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../../dist/experimental/testmode/playwright/msw')
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/proxy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../../dist/experimental/testmode/proxy'
1 change: 1 addition & 0 deletions packages/next/experimental/testmode/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../dist/experimental/testmode/proxy')
11 changes: 10 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@
"headers.d.ts",
"navigation-types",
"web-vitals.js",
"web-vitals.d.ts"
"web-vitals.d.ts",
"experimental/testmode/playwright.js",
"experimental/testmode/playwright.d.ts",
"experimental/testmode/playwright/msw.js",
"experimental/testmode/playwright/msw.d.ts",
"experimental/testmode/proxy.js",
"experimental/testmode/proxy.d.ts"
],
"bin": {
"next": "./dist/bin/next"
Expand Down Expand Up @@ -143,6 +149,7 @@
"@next/react-refresh-utils": "13.4.15",
"@next/swc": "13.4.15",
"@opentelemetry/api": "1.4.1",
"@playwright/test": "^1.35.1",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
Expand Down Expand Up @@ -245,6 +252,7 @@
"lru-cache": "5.1.1",
"micromatch": "4.0.4",
"mini-css-extract-plugin": "2.4.3",
"msw": "^1.2.2",
"nanoid": "3.1.32",
"native-url": "0.3.4",
"neo-async": "2.6.1",
Expand Down Expand Up @@ -283,6 +291,7 @@
"stacktrace-parser": "0.1.10",
"stream-browserify": "3.0.0",
"stream-http": "3.1.1",
"strict-event-emitter": "0.5.0",
"string-hash": "1.1.3",
"string_decoder": "1.3.0",
"strip-ansi": "6.0.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/cli/next-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const nextDev: CliCommand = async (argv) => {
'--hostname': String,
'--turbo': Boolean,
'--experimental-turbo': Boolean,
'--experimental-test-proxy': Boolean,

// To align current messages with native binary.
// Will need to adjust subcommand later.
Expand Down Expand Up @@ -274,13 +275,15 @@ const nextDev: CliCommand = async (argv) => {
// some set-ups that rely on listening on other interfaces
const host = args['--hostname']
config = await loadConfig(PHASE_DEVELOPMENT_SERVER, dir)
const isExperimentalTestProxy = args['--experimental-test-proxy']

const devServerOptions: StartServerOptions = {
dir,
port,
allowRetry,
isDev: true,
hostname: host,
isExperimentalTestProxy,
}

if (args['--turbo']) {
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/cli/next-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const nextStart: CliCommand = async (argv) => {
'--port': Number,
'--hostname': String,
'--keepAliveTimeout': Number,
'--experimental-test-proxy': Boolean,

// Aliases
'-h': '--help',
Expand Down Expand Up @@ -46,6 +47,8 @@ const nextStart: CliCommand = async (argv) => {
const host = args['--hostname']
const port = getPort(args)

const isExperimentalTestProxy = args['--experimental-test-proxy']

const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout']
if (
typeof keepAliveTimeoutArg !== 'undefined' &&
Expand All @@ -66,6 +69,7 @@ const nextStart: CliCommand = async (argv) => {
await startServer({
dir,
isDev: false,
isExperimentalTestProxy,
hostname: host,
port,
keepAliveTimeout,
Expand Down
96 changes: 96 additions & 0 deletions packages/next/src/experimental/testmode/playwright/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Experimental test mode for Playwright

### Prerequisites

You have a Next.js project.

### Install `@playwright/test` in your project

```sh
npm install -D @playwright/test
```

### Optionally install MSW in your project

[MSW](https://mswjs.io/) can be helpful for fetch mocking.

```sh
npm install -D msw
```

### Update `playwright.config.ts`

```javascript
import { defineConfig } from 'next/experimental/testmode/playwright'

export default defineConfig({
webServer: {
command: 'npm dev -- --experimental-test-proxy',
url: 'http://localhost:3000',
},
})
```

### Use the `next/experimental/testmode/playwright` to create tests

```javascript
import { test, expect } from 'next/experimental/testmode/playwright'

test('/product/shoe', async ({ page, next }) => {
next.onFetch((request) => {
if (request.url === 'http://my-db/product/shoe') {
return new Response(
JSON.stringify({
title: 'A shoe',
}),
{
headers: {
'Content-Type': 'application/json',
},
}
)
}
return 'abort'
})

await page.goto('/product/shoe')

await expect(page.locator('body')).toHaveText(/Shoe/)
})
```

### Or use the `next/experimental/testmode/playwright/msw`

```javascript
import { test, expect, rest } from 'next/experimental/testmode/playwright/msw'

test.use({
mswHandlers: [
rest.get('http://my-db/product/shoe', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
title: 'A shoe',
})
)
}),
],
})

test('/product/shoe', async ({ page, msw }) => {
msw.use(
rest.get('http://my-db/product/boot', (req, res, ctx) => {
return res.once(
ctx.status(200),
ctx.json({
title: 'A boot',
})
)
})
)

await page.goto('/product/boot')

await expect(page.locator('body')).toHaveText(/Boot/)
})
```
36 changes: 36 additions & 0 deletions packages/next/src/experimental/testmode/playwright/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { test as base } from '@playwright/test'
import type { NextFixture } from './next-fixture'
import type { NextWorkerFixture } from './next-worker-fixture'
import { applyNextWorkerFixture } from './next-worker-fixture'
import { applyNextFixture } from './next-fixture'

// eslint-disable-next-line import/no-extraneous-dependencies
export * from '@playwright/test'

export type { NextFixture }
export type { FetchHandlerResult } from '../proxy'

export const test = base.extend<
{ next: NextFixture },
{ _nextWorker: NextWorkerFixture }
>({
_nextWorker: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
await applyNextWorkerFixture(use)
},
{ scope: 'worker', auto: true },
],

next: async ({ _nextWorker, page, extraHTTPHeaders }, use, testInfo) => {
await applyNextFixture(use, {
testInfo,
nextWorker: _nextWorker,
page,
extraHTTPHeaders,
})
},
})

export default test
115 changes: 115 additions & 0 deletions packages/next/src/experimental/testmode/playwright/msw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { test as base } from './index'
import type { NextFixture } from './next-fixture'
import {
type RequestHandler,
type MockedResponse,
MockedRequest,
handleRequest,
// eslint-disable-next-line import/no-extraneous-dependencies
} from 'msw'
// eslint-disable-next-line import/no-extraneous-dependencies
import { Emitter } from 'strict-event-emitter'

// eslint-disable-next-line import/no-extraneous-dependencies
export * from 'msw'
// eslint-disable-next-line import/no-extraneous-dependencies
export * from '@playwright/test'
export type { NextFixture }

export interface MswFixture {
use: (...handlers: RequestHandler[]) => void
}

export const test = base.extend<{
msw: MswFixture
mswHandlers: RequestHandler[]
}>({
mswHandlers: [],

msw: [
async ({ next, mswHandlers }, use) => {
const handlers: RequestHandler[] = [...mswHandlers]
const emitter = new Emitter()

next.onFetch(async (request) => {
const {
body,
method,
headers,
credentials,
cache,
redirect,
integrity,
keepalive,
mode,
destination,
referrer,
referrerPolicy,
} = request
const mockedRequest = new MockedRequest(new URL(request.url), {
body: body ? await request.arrayBuffer() : undefined,
method,
headers: Object.fromEntries(headers),
credentials,
cache,
redirect,
integrity,
keepalive,
mode,
destination,
referrer,
referrerPolicy,
})
let isPassthrough = false
let mockedResponse: MockedResponse | undefined
await handleRequest(
mockedRequest,
handlers.slice(0),
{ onUnhandledRequest: 'error' },
emitter as any,
{
onPassthroughResponse: () => {
isPassthrough = true
},
onMockedResponse: (r) => {
mockedResponse = r
},
}
)

if (isPassthrough) {
return 'continue'
}

if (mockedResponse) {
const {
status,
headers: responseHeaders,
body: responseBody,
delay,
} = mockedResponse
if (delay) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
return new Response(responseBody, {
status,
headers: new Headers(responseHeaders),
})
}

return 'abort'
})

await use({
use: (...newHandlers) => {
handlers.unshift(...newHandlers)
},
})

handlers.length = 0
},
{ auto: true },
],
})

export default test
Loading
Loading