Skip to content

Commit

Permalink
Improve experimental test proxy (#63567)
Browse files Browse the repository at this point in the history
Followup on #52520 and
#54014

**Enhancements**
- Removes `--experimental-test-proxy` CLI argument from `next dev` and
`next start`
- Adds a new experimental config option `testProxy?: boolean`
- Instead of throwing an error, return the `originalFetch` response if
the current request context does not contain the `Next-Test-*` HTTP
headers

**Why?**
These changes allow us to write mixed Integration + E2E tests within the
same Playwright process.

```ts
// some-page.spec.ts

test.describe('/some-page', () => {
	test('some integration test', async ({ page, next }) => {
	  // by using the `next` fixture, playwright will send the `Next-Test-*` HTTP headers for 
	  // every request in this test's context.
	  next.onFetch(...);
	  
	  await page.goto(...);
	  await expect(...).toBe('some-mocked-value');
	});

	test('some e2e test', async ({ page }) => {
	  // by NOT using the `next` fixture, playwright does not send the `Next-Test-*` HTTP headers
	  await page.goto(...);
	  await expect(...).toBe('some-real-value');
	});
})
```

Now I can run `next dev` and locally develop my App Router pages AND run
my Playwright tests against instead of having to,
- run `next dev` to locally develop my change
- ctrl+c to kill server
- run `next dev --experimental-test-proxy` to locally run my integration
tests

---------

Co-authored-by: Sam Ko <sam@vercel.com>
  • Loading branch information
agadzik and samcx authored Mar 21, 2024
1 parent 7de7cbf commit 4c467a2
Show file tree
Hide file tree
Showing 13 changed files with 45 additions and 36 deletions.
2 changes: 0 additions & 2 deletions packages/next/src/bin/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ program
'--experimental-upload-trace, <traceUrl>',
'Reports a subset of the debugging trace to a remote HTTP URL. Includes sensitive data.'
)
.addOption(new Option('--experimental-test-proxy').hideHelp())
.action((directory, options, { _optionValueSources }) => {
const portSource = _optionValueSources.port
import('../cli/next-dev.js').then((mod) =>
Expand Down Expand Up @@ -313,7 +312,6 @@ program
'Specify the maximum amount of milliseconds to wait before closing inactive connections.'
).argParser(myParseInt)
)
.addOption(new Option('--experimental-test-proxy').hideHelp())
.action((directory, options) =>
import('../cli/next-start.js').then((mod) =>
mod.nextStart(options, directory)
Expand Down
4 changes: 0 additions & 4 deletions packages/next/src/cli/next-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ type NextDevOptions = {
experimentalHttpsCert?: string
experimentalHttpsCa?: string
experimentalUploadTrace?: string
experimentalTestProxy?: boolean
}

type PortSource = 'cli' | 'default' | 'env'
Expand Down Expand Up @@ -194,8 +193,6 @@ const nextDev = async (

config = await loadConfig(PHASE_DEVELOPMENT_SERVER, dir)

const isExperimentalTestProxy = options.experimentalTestProxy

if (
options.experimentalUploadTrace &&
!process.env.NEXT_TRACE_UPLOAD_DISABLED
Expand All @@ -216,7 +213,6 @@ const nextDev = async (
allowRetry,
isDev: true,
hostname: host,
isExperimentalTestProxy,
}

if (options.turbo) {
Expand Down
3 changes: 0 additions & 3 deletions packages/next/src/cli/next-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ type NextStartOptions = {
port: number
hostname?: string
keepAliveTimeout?: number
experimentalTestProxy?: boolean
}

const nextStart = async (options: NextStartOptions, directory?: string) => {
const dir = getProjectDir(directory)
const host = options.hostname
const port = options.port
const isExperimentalTestProxy = options.experimentalTestProxy
let keepAliveTimeout = options.keepAliveTimeout

if (isPortIsReserved(port)) {
Expand All @@ -30,7 +28,6 @@ const nextStart = async (options: NextStartOptions, directory?: string) => {
await startServer({
dir,
isDev: false,
isExperimentalTestProxy,
hostname: host,
port,
keepAliveTimeout,
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/experimental/testmode/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export async function handleFetch(
): Promise<Response> {
const testInfo = getTestReqInfo(request, reader)
if (!testInfo) {
throw new Error(`No test info for ${request.method} ${request.url}`)
// Passthrough non-test requests.
return originalFetch(request)
}

const { testData, proxyPort } = testInfo
Expand Down
12 changes: 11 additions & 1 deletion packages/next/src/experimental/testmode/playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

You have a Next.js project.

### Update `next.config.js`

```javascript
module.exports = {
experimental: {
testProxy: true,
},
}
```

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

```sh
Expand All @@ -25,7 +35,7 @@ import { defineConfig } from 'next/experimental/testmode/playwright'

export default defineConfig({
webServer: {
command: 'npm dev -- --experimental-test-proxy',
command: 'npm dev',
url: 'http://localhost:3000',
},
})
Expand Down
19 changes: 8 additions & 11 deletions packages/next/src/experimental/testmode/playwright/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,14 @@ export const test = base.test.extend<
{ scope: 'worker', auto: true },
],

next: [
async ({ nextOptions, _nextWorker, page }, use, testInfo) => {
await applyNextFixture(use, {
testInfo,
nextWorker: _nextWorker,
page,
nextOptions,
})
},
{ auto: true },
],
next: async ({ nextOptions, _nextWorker, page }, use, testInfo) => {
await applyNextFixture(use, {
testInfo,
nextWorker: _nextWorker,
page,
nextOptions,
})
},
})

export default test
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
useLightningcss: z.boolean().optional(),
missingSuspenseWithCSRBailout: z.boolean().optional(),
useEarlyImport: z.boolean().optional(),
testProxy: z.boolean().optional(),
})
.optional(),
exportPathMap: z
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,11 @@ export interface ExperimentalConfig {
* Enables early import feature for app router modules
*/
useEarlyImport?: boolean

/**
* Enables `fetch` requests to be proxied to the experimental text proxy server
*/
testProxy?: boolean
}

export type ExportPathMap = {
Expand Down
5 changes: 2 additions & 3 deletions packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export async function initialize(opts: {
isNodeDebugging: boolean
keepAliveTimeout?: number
customServer?: boolean
experimentalTestProxy?: boolean
experimentalHttpsServer?: boolean
startServerSpan?: Span
}): Promise<[WorkerRequestHandler, WorkerUpgradeHandler, NextServer]> {
Expand Down Expand Up @@ -579,7 +578,7 @@ export async function initialize(opts: {
}

let requestHandler: WorkerRequestHandler = requestHandlerImpl
if (opts.experimentalTestProxy) {
if (config.experimental.testProxy) {
// Intercept fetch and other testmode apis.
const {
wrapRequestHandlerWorker,
Expand All @@ -599,7 +598,7 @@ export async function initialize(opts: {
server: opts.server,
isNodeDebugging: !!opts.isNodeDebugging,
serverFields: developmentBundler?.serverFields || {},
experimentalTestProxy: !!opts.experimentalTestProxy,
experimentalTestProxy: !!config.experimental.testProxy,
experimentalHttpsServer: !!opts.experimentalHttpsServer,
bundlerService: devBundlerService,
startServerSpan: opts.startServerSpan,
Expand Down
6 changes: 0 additions & 6 deletions packages/next/src/server/lib/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface StartServerOptions {
keepAliveTimeout?: number
// this is dev-server only
selfSignedCertificate?: SelfSignedCertificate
isExperimentalTestProxy?: boolean
}

export async function getRequestHandlers({
Expand All @@ -52,7 +51,6 @@ export async function getRequestHandlers({
minimalMode,
isNodeDebugging,
keepAliveTimeout,
experimentalTestProxy,
experimentalHttpsServer,
}: {
dir: string
Expand All @@ -63,7 +61,6 @@ export async function getRequestHandlers({
minimalMode?: boolean
isNodeDebugging?: boolean
keepAliveTimeout?: number
experimentalTestProxy?: boolean
experimentalHttpsServer?: boolean
}): ReturnType<typeof initialize> {
return initialize({
Expand All @@ -75,7 +72,6 @@ export async function getRequestHandlers({
server,
isNodeDebugging: isNodeDebugging || false,
keepAliveTimeout,
experimentalTestProxy,
experimentalHttpsServer,
startServerSpan,
})
Expand All @@ -91,7 +87,6 @@ export async function startServer(
minimalMode,
allowRetry,
keepAliveTimeout,
isExperimentalTestProxy,
selfSignedCertificate,
} = serverOptions
let { port } = serverOptions
Expand Down Expand Up @@ -302,7 +297,6 @@ export async function startServer(
minimalMode,
isNodeDebugging: Boolean(nodeDebugType),
keepAliveTimeout,
experimentalTestProxy: !!isExperimentalTestProxy,
experimentalHttpsServer: !!selfSignedCertificate,
})
requestHandler = initResult[0]
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/testmode/app/app/rsc-fetch/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export async function generateMetadata() {
}

export default async function Page() {
const text = await (await fetch('https://example.com')).text()
const text = await (
await fetch('https://next-data-api-endpoint.vercel.app/api/random')
).text()
return <pre>{text}</pre>
}
4 changes: 4 additions & 0 deletions test/e2e/testmode/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
testProxy: true,
},
rewrites() {
return [
{
Expand Down
13 changes: 9 additions & 4 deletions test/e2e/testmode/testmode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ createNextDescribe(
files: __dirname,
skipDeployment: true,
dependencies: require('./package.json').dependencies,
startCommand: (global as any).isNextDev
? 'pnpm next dev --experimental-test-proxy'
: 'pnpm next start --experimental-test-proxy',
},
({ next, isNextDev }) => {
let proxyServer: Awaited<ReturnType<typeof createProxyServer>>
Expand All @@ -19,7 +16,10 @@ createNextDescribe(
onFetch: async (testData, request) => {
if (
request.method === 'GET' &&
request.url === 'https://example.com/'
[
'https://example.com/',
'https://next-data-api-endpoint.vercel.app/api/random',
].includes(request.url)
) {
return new Response(testData)
}
Expand Down Expand Up @@ -48,6 +48,11 @@ createNextDescribe(
}

describe('app router', () => {
it('should fetch real data when Next-Test-* headers are not present', async () => {
const html = await (await next.fetch('/app/rsc-fetch')).text()
expect(html).not.toContain('<pre>test1</pre>')
})

it('should handle RSC with fetch in serverless function', async () => {
const html = await (await fetchForTest('/app/rsc-fetch')).text()
expect(html).toContain('<pre>test1</pre>')
Expand Down

0 comments on commit 4c467a2

Please sign in to comment.