|
7 | 7 | expect, |
8 | 8 | type Locator, |
9 | 9 | type Page, |
| 10 | + type Request, |
10 | 11 | type Video, |
11 | 12 | webkit, |
12 | 13 | } from '@playwright/test' |
@@ -213,6 +214,105 @@ try { |
213 | 214 | } finally { |
214 | 215 | await noScript.close() |
215 | 216 | } |
| 217 | + const prefetchContext = await browser.newContext({ |
| 218 | + viewport: { width: 390, height: 844 }, |
| 219 | + reducedMotion: 'reduce', |
| 220 | + }) |
| 221 | + try { |
| 222 | + await check( |
| 223 | + 'In-flight intent prefetch transfers a large reference body only once', |
| 224 | + async () => { |
| 225 | + const page = await prefetchContext.newPage() |
| 226 | + const destination = '/integrations/cloudflare' |
| 227 | + const requests: Array<{ request: Request; beforeClick: boolean }> = [] |
| 228 | + const finished = new Set<Request>() |
| 229 | + const transfers: Array<{ |
| 230 | + url: string |
| 231 | + beforeClick: boolean |
| 232 | + segment?: string |
| 233 | + prefetch?: string |
| 234 | + status: number |
| 235 | + encodedBytes: number |
| 236 | + }> = [] |
| 237 | + let clicked = false |
| 238 | + let inFlightAtClick = false |
| 239 | + try { |
| 240 | + await open(page, '/integrations/apollo') |
| 241 | + await page.getByRole('button', { name: 'Menu', exact: true }).click() |
| 242 | + const link = page |
| 243 | + .getByRole('navigation', { name: 'Documentation sidebar', exact: true }) |
| 244 | + .getByRole('link', { name: 'Cloudflare', exact: true }) |
| 245 | + await link.scrollIntoViewIfNeeded() |
| 246 | + const session = await prefetchContext.newCDPSession(page) |
| 247 | + await session.send('Network.enable') |
| 248 | + await session.send('Network.emulateNetworkConditions', { |
| 249 | + offline: false, |
| 250 | + latency: 150, |
| 251 | + downloadThroughput: 100 * 1024, |
| 252 | + uploadThroughput: 100 * 1024, |
| 253 | + }) |
| 254 | + const isDestination = (request: Request) => |
| 255 | + new URL(request.url()).pathname === destination && request.headers().rsc === '1' |
| 256 | + page.on('request', (request) => { |
| 257 | + if (isDestination(request)) requests.push({ request, beforeClick: !clicked }) |
| 258 | + }) |
| 259 | + page.on('requestfinished', (request) => finished.add(request)) |
| 260 | + const prefetched = page.waitForRequest((request) => { |
| 261 | + if (!isDestination(request)) return false |
| 262 | + const segment = request.headers()['next-router-segment-prefetch'] |
| 263 | + return !segment || segment.endsWith('/__PAGE__') |
| 264 | + }) |
| 265 | + await link.focus() |
| 266 | + const pending = await prefetched |
| 267 | + inFlightAtClick = !finished.has(pending) |
| 268 | + expect(inFlightAtClick, 'The regression must navigate during a real body transfer').toBe( |
| 269 | + true |
| 270 | + ) |
| 271 | + clicked = true |
| 272 | + await link.click() |
| 273 | + await expect( |
| 274 | + page.getByRole('heading', { level: 1, name: 'Cloudflare', exact: true }) |
| 275 | + ).toBeVisible({ timeout: 15000 }) |
| 276 | + await expect(page).toHaveURL(new URL(destination, target).href) |
| 277 | + await expect(page.locator('main article')).not.toBeEmpty() |
| 278 | + for (const { request, beforeClick } of requests) { |
| 279 | + const response = await request.response() |
| 280 | + if (!response) |
| 281 | + throw new Error(`Destination request failed: ${request.failure()?.errorText}`) |
| 282 | + const failure = await response.finished() |
| 283 | + if (failure) throw failure |
| 284 | + const sizes = await request.sizes() |
| 285 | + transfers.push({ |
| 286 | + url: request.url(), |
| 287 | + beforeClick, |
| 288 | + segment: request.headers()['next-router-segment-prefetch'], |
| 289 | + prefetch: request.headers()['next-router-prefetch'], |
| 290 | + status: response.status(), |
| 291 | + encodedBytes: sizes.responseBodySize, |
| 292 | + }) |
| 293 | + expect(response.ok()).toBe(true) |
| 294 | + } |
| 295 | + const bodies = transfers.filter((transfer) => transfer.encodedBytes > 64 * 1024) |
| 296 | + expect( |
| 297 | + bodies, |
| 298 | + 'Navigation must reuse the large in-flight prefetched response' |
| 299 | + ).toHaveLength(1) |
| 300 | + expect(bodies[0]?.beforeClick, 'The large response must originate from intent').toBe(true) |
| 301 | + } finally { |
| 302 | + await writeFile( |
| 303 | + join(directory, 'in-flight-prefetch.json'), |
| 304 | + `${JSON.stringify({ source: '/integrations/apollo', destination, latencyMs: 150, bytesPerSecond: 100 * 1024, inFlightAtClick, transfers }, null, 2)}\n` |
| 305 | + ) |
| 306 | + await page.close() |
| 307 | + } |
| 308 | + }, |
| 309 | + engine === 'webkit' |
| 310 | + ? 'Controlled network throttling uses Chromium CDP; Playwright WebKit cannot exercise this transport regression.' |
| 311 | + : undefined |
| 312 | + ) |
| 313 | + } finally { |
| 314 | + await prefetchContext.close() |
| 315 | + } |
216 | 316 | const sidebarContext = await browser.newContext({ |
217 | 317 | viewport: { width: 1440, height: 640 }, |
218 | 318 | reducedMotion: 'reduce', |
@@ -1308,7 +1408,7 @@ try { |
1308 | 1408 | await browser?.close() |
1309 | 1409 | await writeFile( |
1310 | 1410 | reportPath, |
1311 | | - `${JSON.stringify({ origin: target.origin, engine, limitations: engine === 'webkit' ? ['Clipboard readback and reduced-transparency emulation remain Chromium-only; this is Playwright WebKit, not native Safari.'] : [], results }, null, 2)}\n` |
| 1411 | + `${JSON.stringify({ origin: target.origin, engine, limitations: engine === 'webkit' ? ['Clipboard readback, reduced-transparency emulation and controlled network throttling remain Chromium-only; this is Playwright WebKit, not native Safari.'] : [], results }, null, 2)}\n` |
1312 | 1412 | ) |
1313 | 1413 | } |
1314 | 1414 | logger.info('Verified documentation navigation', { checks: results.length, reportPath }) |
|
0 commit comments