-
Notifications
You must be signed in to change notification settings - Fork 27.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Test Mode: report onFetch interceptions in the test (#55456)
This feature eases debugging significantly. This is what an intercepted fetch looks like in Playwright debugger: <img width="342" alt="image" src="https://github.com/vercel/next.js/assets/726049/e9fe4304-36b9-4d6d-b4f3-66d649464a35"> <img width="368" alt="image" src="https://github.com/vercel/next.js/assets/726049/db49e18e-3fc5-4a77-abf8-465d925082bc"> And here's what a failing fetch looks like: <img width="314" alt="image" src="https://github.com/vercel/next.js/assets/726049/41be212a-e414-4b28-a0cb-d9b618b3f2ea"> You can inspect request and response's headers and bodies. The main drawback: it uses an internal Playwright API. However, they are open to opening part of it publicly in microsoft/playwright#27059.
- Loading branch information
Showing
4 changed files
with
209 additions
and
19 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
115 changes: 115 additions & 0 deletions
115
packages/next/src/experimental/testmode/playwright/report.ts
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,115 @@ | ||
import type { TestInfo } from '@playwright/test' | ||
import type { FetchHandler } from './next-worker-fixture' | ||
import { step } from './step' | ||
|
||
async function parseBody( | ||
r: Pick<Request, 'headers' | 'json' | 'text' | 'arrayBuffer' | 'formData'> | ||
): Promise<Record<string, string>> { | ||
const contentType = r.headers.get('content-type') | ||
let error: string | undefined | ||
let text: string | undefined | ||
let json: unknown | ||
let formData: FormData | undefined | ||
let buffer: ArrayBuffer | undefined | ||
if (contentType?.includes('text')) { | ||
try { | ||
text = await r.text() | ||
} catch (e) { | ||
error = 'failed to parse text' | ||
} | ||
} else if (contentType?.includes('json')) { | ||
try { | ||
json = await r.json() | ||
} catch (e) { | ||
error = 'failed to parse json' | ||
} | ||
} else if (contentType?.includes('form-data')) { | ||
try { | ||
formData = await r.formData() | ||
} catch (e) { | ||
error = 'failed to parse formData' | ||
} | ||
} else { | ||
try { | ||
buffer = await r.arrayBuffer() | ||
} catch (e) { | ||
error = 'failed to parse arrayBuffer' | ||
} | ||
} | ||
return { | ||
...(error ? { error } : null), | ||
...(text ? { text } : null), | ||
...(json ? { json: JSON.stringify(json) } : null), | ||
...(formData ? { formData: JSON.stringify(Array.from(formData)) } : null), | ||
...(buffer && buffer.byteLength > 0 | ||
? { buffer: `base64;${Buffer.from(buffer).toString('base64')}` } | ||
: null), | ||
} | ||
} | ||
|
||
function parseHeaders(headers: Headers): Record<string, string> { | ||
return Object.fromEntries( | ||
Array.from(headers) | ||
.sort(([key1], [key2]) => key1.localeCompare(key2)) | ||
.map(([key, value]) => { | ||
return [`header.${key}`, value] | ||
}) | ||
) | ||
} | ||
|
||
export async function reportFetch( | ||
testInfo: TestInfo, | ||
req: Request, | ||
handler: FetchHandler | ||
): Promise<Awaited<ReturnType<FetchHandler>>> { | ||
return step( | ||
testInfo, | ||
{ | ||
title: `next.onFetch: ${req.method} ${req.url}`, | ||
category: 'next.onFetch', | ||
apiName: 'next.onFetch', | ||
params: { | ||
method: req.method, | ||
url: req.url, | ||
...(await parseBody(req.clone())), | ||
...parseHeaders(req.headers), | ||
}, | ||
}, | ||
async (complete) => { | ||
const res = await handler(req) | ||
if (res === undefined || res == null) { | ||
complete({ error: { message: 'unhandled' } }) | ||
} else if (typeof res === 'string' && res !== 'continue') { | ||
complete({ error: { message: res } }) | ||
} else { | ||
let body: Record<string, unknown> | ||
if (typeof res === 'string') { | ||
body = { response: res } | ||
} else { | ||
const { status, statusText } = res | ||
body = { | ||
status, | ||
...(statusText ? { statusText } : null), | ||
...(await parseBody(res.clone())), | ||
...parseHeaders(res.headers), | ||
} | ||
} | ||
await step( | ||
testInfo, | ||
{ | ||
title: `next.onFetch.fulfilled: ${req.method} ${req.url}`, | ||
category: 'next.onFetch', | ||
apiName: 'next.onFetch.fulfilled', | ||
params: { | ||
...body, | ||
'request.url': req.url, | ||
'request.method': req.method, | ||
}, | ||
}, | ||
async () => undefined | ||
).catch(() => undefined) | ||
} | ||
return res | ||
} | ||
) | ||
} |
57 changes: 57 additions & 0 deletions
57
packages/next/src/experimental/testmode/playwright/step.ts
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,57 @@ | ||
import type { TestInfo } from '@playwright/test' | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { test } from '@playwright/test' | ||
|
||
export interface StepProps { | ||
category: string | ||
title: string | ||
apiName?: string | ||
params?: Record<string, string | number | boolean | null | undefined> | ||
} | ||
|
||
// Access the internal Playwright API until it's exposed publicly. | ||
// See https://github.com/microsoft/playwright/issues/27059. | ||
interface TestInfoWithRunAsStep extends TestInfo { | ||
_runAsStep: <T>( | ||
stepInfo: StepProps, | ||
handler: (result: { complete: Complete }) => Promise<T> | ||
) => Promise<T> | ||
} | ||
|
||
type Complete = (result: { error?: any }) => void | ||
|
||
function isWithRunAsStep( | ||
testInfo: TestInfo | ||
): testInfo is TestInfoWithRunAsStep { | ||
return '_runAsStep' in testInfo | ||
} | ||
|
||
export async function step<T>( | ||
testInfo: TestInfo, | ||
props: StepProps, | ||
handler: (complete: Complete) => Promise<Awaited<T>> | ||
): Promise<Awaited<T>> { | ||
if (isWithRunAsStep(testInfo)) { | ||
return testInfo._runAsStep(props, ({ complete }) => handler(complete)) | ||
} | ||
|
||
// Fallback to the `test.step()`. | ||
let result: Awaited<T> | ||
let reportedError: any | ||
try { | ||
console.log(props.title, props) | ||
await test.step(props.title, async () => { | ||
result = await handler(({ error }) => { | ||
reportedError = error | ||
if (reportedError) { | ||
throw reportedError | ||
} | ||
}) | ||
}) | ||
} catch (error) { | ||
if (error !== reportedError) { | ||
throw error | ||
} | ||
} | ||
return result! | ||
} |
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