Repository: vercel/next.js
Pull request: Fix potential race condition in createRouterAct

A few of the tests that use `createRouterAct` occasionally flake in CI. I'm not exactly sure why because I'm struggling to reproduce them locally, but my best guess based on some of the logs is that there's a race condition between the IntersectionObserver event that triggers the prefetch request, and the async event scheduled by `createRouterAct` that is meant to always run after that.

I had thought `requestAnimationFrame` would be a good way to ensure that the event runs after the IntersectionObserver, but I think that's not always correct and depends on the timing of the paint/layout cycle. So I've switched it to `requestIdleCallback` instead since that won't run until the rest of the event queue is empty.

(A timer would obviously work here, too, but we should avoid using timers except to timeout a failing test, because they slow down CI.)

I've also added better error message to `createRouterAct` in the case where an unexpected request is initiated. The updated message now includes both the URL and the response body.

Changed files:
filename: test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts
patch: @@ -159,12 +159,21 @@ describe('segment cache (incremental opt in)', () => {
         { includes: 'Page content', block: 'reject' },
       ])
 
-      await act(async () => {
-        const checkbox = await browser.elementByCss(
-          `input[data-link-accordion="/ppr-disabled-with-loading-boundary/child"]`
-        )
-        await checkbox.click()
-      }, 'no-requests')
+      // When prefetching a different page with the same loading boundary,
+      // we should not need to fetch the loading boundary again
+      await act(
+        async () => {
+          const checkbox = await browser.elementByCss(
+            `input[data-link-accordion="/ppr-disabled-with-loading-boundary/child"]`
+          )
+          await checkbox.click()
+        },
+        // This assertion will fail if more than one request includes the given
+        // string. Because the string appears in the FlightRouterState for the
+        // page, it effectively asserts that only one prefetch request is issued
+        // — the one for the route tree.
+        { includes: 'ppr-disabled-with-loading-boundary' }
+      )
 
       // Navigate to the page
       await act(async () => {

filename: test/e2e/app-dir/segment-cache/router-act.ts
patch: @@ -223,11 +223,11 @@ export function createRouterAct(
       // that comes after the scope function, rather than immediately when the
       // scope function exits.
       //
-      // We use requestAnimationFrame to schedule the task because that's
+      // We use requestIdleCallback to schedule the task because that's
       // guaranteed to fire after any IntersectionObserver events, which the
       // router uses to track the visibility of links.
       await page.evaluate(
-        () => new Promise<void>((res) => requestAnimationFrame(() => res()))
+        () => new Promise<void>((res) => requestIdleCallback(() => res()))
       )
 
       // Checking whether a request needs to be intercepted is an async
@@ -254,7 +254,16 @@ export function createRouterAct(
           } else {
             item.didProcess = true
             if (expectedResponses === null) {
-              error.message = 'Expected no network requests to be initiated.'
+              error.message = `
+Expected no network requests to be initiated.
+
+URL: ${request.url()}
+Headers: ${JSON.stringify(fulfilled.headers)}
+
+Response:
+${fulfilled.body}
+`
+
               throw error
             }
             if (expectedResponses !== null) {