Repository: vercel/next.js
Pull request: [tests]: de-flake client-cache.parallel-routes.test.ts

Saw this flaking when deployed. This switches it to use `router-act` which more consistently passes in my testing. 

Changed files:
filename: test/e2e/app-dir/app-client-cache/client-cache.parallel-routes.test.ts
patch: @@ -1,12 +1,6 @@
 import { nextTestSetup } from 'e2e-utils'
-import { check } from 'next-test-utils'
-import { Playwright } from 'next-webdriver'
-import {
-  browserConfigWithFixedTime,
-  createRequestsListener,
-  fastForwardTo,
-  getPathname,
-} from './test-utils'
+import { createRouterAct } from 'router-act'
+import { browserConfigWithFixedTime, fastForwardTo } from './test-utils'
 import path from 'path'
 
 describe('app dir client cache with parallel routes', () => {
@@ -21,62 +15,120 @@ describe('app dir client cache with parallel routes', () => {
   }
 
   describe('prefetch={true}', () => {
-    let browser: Playwright
-
-    beforeEach(async () => {
-      browser = await next.browser('/', browserConfigWithFixedTime)
-    })
-
     it('should prefetch the full page', async () => {
-      const { getRequests, clearRequests } =
-        await createRequestsListener(browser)
-      await check(() => {
-        return getRequests().some(
-          ([url, didPartialPrefetch]) =>
-            getPathname(url) === '/0' && !didPartialPrefetch
-        )
-          ? 'success'
-          : 'fail'
-      }, 'success')
-
-      clearRequests()
-
-      await browser
-        .elementByCss('[href="/0"]')
-        .click()
-        .waitForElementByCss('#random-number')
-
-      expect(getRequests().every(([url]) => getPathname(url) !== '/0')).toEqual(
-        true
+      let act: ReturnType<typeof createRouterAct>
+      const browser = await next.browser('/', {
+        beforePageLoad(page) {
+          browserConfigWithFixedTime.beforePageLoad(page)
+          act = createRouterAct(page)
+        },
+      })
+
+      // Reveal the link to trigger prefetch and wait for it to complete
+      const link = await act(
+        async () => {
+          const reveal = await browser.elementByCss(
+            '[data-link-accordion="/0"]'
+          )
+          await reveal.click()
+          return await browser.elementByCss('[href="/0"]')
+        },
+        { includes: 'random-number' }
       )
+
+      // Navigate to /0 - should not make additional requests
+      await act(async () => {
+        await link.click()
+        await browser.waitForElementByCss('#random-number')
+      }, 'no-requests')
     })
 
     it('should re-use the cache for the full page, only for 5 mins', async () => {
-      const randomNumber = await browser
-        .elementByCss('[href="/0"]')
-        .click()
-        .waitForElementByCss('#random-number')
-        .text()
+      let act: ReturnType<typeof createRouterAct>
+      const browser = await next.browser('/', {
+        beforePageLoad(page) {
+          browserConfigWithFixedTime.beforePageLoad(page)
+          act = createRouterAct(page)
+        },
+      })
+
+      // Toggle the link, assert on the prefetch content
+      const link = await act(
+        async () => {
+          const reveal = await browser.elementByCss(
+            '[data-link-accordion="/0"]'
+          )
+          await reveal.click()
+          return await browser.elementByCss('[href="/0"]')
+        },
+        { includes: 'random-number' }
+      )
 
-      await browser.elementByCss('[href="/"]').click()
+      // Navigate to the page, assert no requests are made
+      const randomNumber = await act(async () => {
+        await link.click()
+        await browser.waitForElementByCss('#random-number')
+        return await browser.elementByCss('#random-number').text()
+      }, 'no-requests')
+
+      // Toggle the home link, assert on the homepage content
+      const homeLink = await act(
+        async () => {
+          const reveal = await browser.elementByCss('[data-link-accordion="/"]')
+          await reveal.click()
+          return await browser.elementByCss('[href="/"]')
+        },
+        { includes: 'home-page' }
+      )
 
-      const number = await browser
-        .elementByCss('[href="/0"]')
-        .click()
-        .waitForElementByCss('#random-number')
-        .text()
+      // Navigate home, assert no requests are made
+      await act(async () => {
+        await homeLink.click()
+        await browser.waitForElementByCss('#home-page')
+      }, 'no-requests')
+
+      // Toggle the link to the other page again, navigate, assert no requests (because it's cached)
+      const number = await act(async () => {
+        const reveal = await browser.elementByCss('[data-link-accordion="/0"]')
+        await reveal.click()
+        const link = await browser.elementByCss('[href="/0"]')
+        await link.click()
+        await browser.waitForElementByCss('#random-number')
+        return await browser.elementByCss('#random-number').text()
+      }, 'no-requests')
 
       expect(number).toBe(randomNumber)
 
+      // Navigate back home
+      await act(async () => {
+        const reveal = await browser.elementByCss('[data-link-accordion="/"]')
+        await reveal.click()
+        const homeLink = await browser.elementByCss('[href="/"]')
+        await homeLink.click()
+        await browser.waitForElementByCss('#home-page')
+      }, 'no-requests')
+
+      // Fast forward 5 minutes
       await browser.eval(fastForwardTo, 5 * 60 * 1000)
 
-      await browser.elementByCss('[href="/"]').click()
+      // Toggle the link to the other page again, assert on prefetch content
+      const linkAfterExpiry = await act(
+        async () => {
+          const reveal = await browser.elementByCss(
+            '[data-link-accordion="/0"]'
+          )
+          await reveal.click()
+          return await browser.elementByCss('[href="/0"]')
+        },
+        { includes: 'random-number' }
+      )
 
-      const newNumber = await browser
-        .elementByCss('[href="/0"]')
-        .click()
-        .waitForElementByCss('#random-number')
-        .text()
+      // Navigate to the page and verify the content is fresh (different from cached)
+      const newNumber = await act(async () => {
+        await linkAfterExpiry.click()
+        await browser.waitForElementByCss('#random-number')
+        return await browser.elementByCss('#random-number').text()
+      }, 'no-requests')
 
       expect(newNumber).not.toBe(randomNumber)
     })

filename: test/e2e/app-dir/app-client-cache/fixtures/parallel-routes/app/[id]/page.js
patch: @@ -1,4 +1,4 @@
-import Link from 'next/link'
+import { LinkAccordion } from '../components/link-accordion'
 
 export default async function Page() {
   const randomNumber = await new Promise((resolve) => {
@@ -8,11 +8,9 @@ export default async function Page() {
   })
 
   return (
-    <>
-      <div>
-        <Link href="/">Back to Home</Link>
-      </div>
+    <div id="dynamic-page">
+      <LinkAccordion href="/">Back to Home</LinkAccordion>
       <div id="random-number">{randomNumber}</div>
-    </>
+    </div>
   )
 }

filename: test/e2e/app-dir/app-client-cache/fixtures/parallel-routes/app/components/link-accordion.tsx
patch: @@ -0,0 +1,25 @@
+'use client'
+
+import Link from 'next/link'
+import { useState } from 'react'
+
+export function LinkAccordion({ href, children, prefetch = undefined }) {
+  const [isVisible, setIsVisible] = useState(false)
+  return (
+    <>
+      <input
+        type="checkbox"
+        checked={isVisible}
+        onChange={() => setIsVisible(!isVisible)}
+        data-link-accordion={href}
+      />
+      {isVisible ? (
+        <Link href={href} prefetch={prefetch}>
+          {children}
+        </Link>
+      ) : (
+        <>{children} (link is hidden)</>
+      )}
+    </>
+  )
+}

filename: test/e2e/app-dir/app-client-cache/fixtures/parallel-routes/app/page.js
patch: @@ -1,11 +1,11 @@
-import Link from 'next/link'
+import { LinkAccordion } from './components/link-accordion'
 
 export default function Page() {
   return (
-    <div>
-      <Link href="/0" prefetch={true}>
+    <div id="home-page">
+      <LinkAccordion href="/0" prefetch={true}>
         To Dynamic Page
-      </Link>
+      </LinkAccordion>
     </div>
   )
 }