Loading...
Loading...
How to write end-to-end tests using createRouterAct and LinkAccordion. Use when writing or modifying tests that need to control the timing of internal Next.js requests (like prefetches) or assert on their responses. Covers the act API, fixture patterns, prefetch control via LinkAccordion, fake clocks, and avoiding flaky testing patterns.
npx skill4agent add vercel/next.js router-actcreateRouterActtest/lib/router-act.tsactactbrowser.elementById()browser.elementByCss()browser.waitForElementByCss()LinkAccordionact'no-requests'actretry()setTimeoutactblockincludes'no-requests'// Assert NO router requests are made (data served from cache).
// Prefer this whenever possible — it's the strongest assertion.
await act(async () => { ... }, 'no-requests')
// Expect at least one response containing this substring
await act(async () => { ... }, { includes: 'Page content' })
// Expect multiple responses (checked in order)
await act(async () => { ... }, [
{ includes: 'First response' },
{ includes: 'Second response' },
])
// Assert the same content appears in two separate responses
await act(async () => { ... }, [
{ includes: 'Repeated content' },
{ includes: 'Repeated content' },
])
// Expect at least one request, don't assert on content
await act(async () => { ... })includesincludes'Dynamic content (stale time 60s)'includesincludes{ includes: '...' }actactrequestIdleCallbackactactawait act(
async () => {
/* toggle accordion, click link */
},
{ includes: 'Page content' }
)
// Read content after act returns, not inside the scope
expect(await browser.elementById('my-content').text()).toBe('Page content')LinkAccordion<Link><Link>act// components/link-accordion.tsx
'use client'
import Link from 'next/link'
import { useState } from 'react'
export function LinkAccordion({ href, children, prefetch }) {
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)`
)}
</>
)
}actawait act(
async () => {
// 1. Toggle accordion — Link enters DOM, triggers prefetch
const toggle = await browser.elementByCss(
'input[data-link-accordion="/target-page"]'
)
await toggle.click()
// 2. Click the now-visible link — triggers navigation
const link = await browser.elementByCss('a[href="/target-page"]')
await link.click()
},
{ includes: 'Expected page content' }
)browser.back()browser.back()useStateactno-requestsbrowser.back()browser.forward()<Link>act<Link>actLinkAccordionactretry()setTimeoutactactactbrowser.back()LinkAccordionconnection()act// app/my-test/hub-a/page.tsx
import { Suspense } from 'react'
import { connection } from 'next/server'
import { LinkAccordion } from '../../components/link-accordion'
async function Content() {
await connection()
return <div id="hub-a-content">Hub a</div>
}
export default function Page() {
return (
<>
<Suspense fallback="Loading...">
<Content />
</Suspense>
<ul>
<li>
<LinkAccordion href="/my-test/target-page">Target page</LinkAccordion>
</li>
</ul>
</>
)
}// On target pages, add LinkAccordion links to hub pages
<LinkAccordion href="/my-test/hub-a">Hub A</LinkAccordion>// 1. Navigate to target (first visit)
await act(
async () => {
/* toggle accordion, click link */
},
{ includes: 'Target content' }
)
// 2. Navigate to hub-a (fresh page, all accordions closed)
await act(
async () => {
const toggle = await browser.elementByCss(
'input[data-link-accordion="/my-test/hub-a"]'
)
await toggle.click()
const link = await browser.elementByCss('a[href="/my-test/hub-a"]')
await link.click()
},
{ includes: 'Hub a' }
)
// 3. Advance time
await page.clock.setFixedTime(startDate + 60 * 1000)
// 4. Navigate back to target from hub (controlled prefetch)
await act(async () => {
const toggle = await browser.elementByCss(
'input[data-link-accordion="/my-test/target-page"]'
)
await toggle.click()
const link = await browser.elementByCss('a[href="/my-test/target-page"]')
await link.click()
}, 'no-requests') // or { includes: '...' } if data is staleDate.now()async function startBrowserWithFakeClock(url: string) {
let page!: Playwright.Page
const startDate = Date.now()
const browser = await next.browser(url, {
async beforePageLoad(p: Playwright.Page) {
page = p
await page.clock.install()
await page.clock.setFixedTime(startDate)
},
})
const act = createRouterAct(page)
return { browser, page, act, startDate }
}setFixedTimeDate.now()Date.now()setFixedTimesetTimeoutsetIntervalcreateRouterActtest/lib/router-act.tsLinkAccordiontest/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsxtest/e2e/app-dir/segment-cache/staleness/