Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
434 lines
14 KiB
TypeScript
434 lines
14 KiB
TypeScript
import type * as Playwright from 'playwright'
|
|
import { isNextDev, isNextDeploy, createNext } from 'e2e-utils'
|
|
import { createRouterAct } from 'router-act'
|
|
import { createTestDataServer } from 'test-data-service/writer'
|
|
import { createTestLog } from 'test-log'
|
|
import { findPort } from 'next-test-utils'
|
|
|
|
describe('segment cache (revalidation)', () => {
|
|
if (isNextDev || isNextDeploy) {
|
|
test('disabled in development / deployment', () => {})
|
|
return
|
|
}
|
|
|
|
let port = -1
|
|
let server
|
|
let dataVersions = new Map()
|
|
let TestLog = createTestLog()
|
|
|
|
let next
|
|
beforeAll(async () => {
|
|
port = await findPort()
|
|
server = createTestDataServer(async (key, res) => {
|
|
const currentVersion = dataVersions.get(key)
|
|
|
|
// Increment the version number each time to track how often the
|
|
// server renders.
|
|
const nextVersion = currentVersion === undefined ? 1 : currentVersion + 1
|
|
dataVersions.set(key, nextVersion)
|
|
|
|
// Append the version number to the response
|
|
const response = `${key} [${nextVersion}]`
|
|
TestLog.log('REQUEST: ' + key)
|
|
res.resolve(response)
|
|
})
|
|
server.listen(port)
|
|
|
|
next = await createNext({
|
|
files: __dirname,
|
|
env: { TEST_DATA_SERVICE_URL: `http://localhost:${port}` },
|
|
})
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
dataVersions = new Map()
|
|
TestLog = createTestLog()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await next?.destroy()
|
|
server?.close()
|
|
})
|
|
|
|
it('evict client cache when Server Action calls revalidatePath', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
let page: Playwright.Page
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const linkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-link-accordion="/greeting"]'
|
|
)
|
|
|
|
// Reveal the link the target page to trigger a prefetch
|
|
await act(
|
|
async () => {
|
|
await linkVisibilityToggle.click()
|
|
},
|
|
{
|
|
includes: 'random-greeting',
|
|
}
|
|
)
|
|
|
|
// Install fake timers — freezes the 300ms cooldown setTimeout
|
|
await page.clock.install()
|
|
|
|
// Perform an action that calls revalidatePath. This should cause the
|
|
// corresponding entry to be evicted from the client cache, and a new
|
|
// prefetch to be requested.
|
|
await act(async () => {
|
|
const revalidateByPath = await browser.elementById('revalidate-by-path')
|
|
await revalidateByPath.click()
|
|
})
|
|
|
|
// Advance past cooldown inside act() to intercept the re-prefetch
|
|
await act(
|
|
async () => {
|
|
await page.clock.fastForward(300)
|
|
},
|
|
{
|
|
includes: 'random-greeting [1]',
|
|
}
|
|
)
|
|
TestLog.assert(['REQUEST: random-greeting'])
|
|
|
|
// Navigate to the target page.
|
|
await act(async () => {
|
|
const link = await browser.elementByCss('a[href="/greeting"]')
|
|
await link.click()
|
|
// Navigation should finish immedately because the page is
|
|
// fully prefetched.
|
|
const greeting = await browser.elementById('greeting')
|
|
expect(await greeting.innerHTML()).toBe('random-greeting [1]')
|
|
}, 'no-requests')
|
|
})
|
|
|
|
it('refetch visible Form components after cache is revalidated', async () => {
|
|
// This is the same as the previous test, but for forms. Since the
|
|
// prefetching implementation is shared between Link and Form, we don't
|
|
// bother to test every feature using both Link and Form; this test should
|
|
// be sufficient.
|
|
let act: ReturnType<typeof createRouterAct>
|
|
let page: Playwright.Page
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const formVisibilityToggle = await browser.elementByCss(
|
|
'input[data-form-accordion="/greeting"]'
|
|
)
|
|
|
|
// Reveal the form that points to the target page to trigger a prefetch
|
|
await act(
|
|
async () => {
|
|
await formVisibilityToggle.click()
|
|
},
|
|
{
|
|
includes: 'random-greeting',
|
|
}
|
|
)
|
|
|
|
// Install fake timers — freezes the 300ms cooldown setTimeout
|
|
await page.clock.install()
|
|
|
|
// Perform an action that calls revalidatePath. This should cause the
|
|
// corresponding entry to be evicted from the client cache, and a new
|
|
// prefetch to be requested.
|
|
await act(async () => {
|
|
const revalidateByPath = await browser.elementById('revalidate-by-path')
|
|
await revalidateByPath.click()
|
|
})
|
|
|
|
// Advance past cooldown inside act() to intercept the re-prefetch
|
|
await act(
|
|
async () => {
|
|
await page.clock.fastForward(300)
|
|
},
|
|
{
|
|
includes: 'random-greeting [1]',
|
|
}
|
|
)
|
|
TestLog.assert(['REQUEST: random-greeting'])
|
|
|
|
// Navigate to the target page.
|
|
await act(async () => {
|
|
const button = await browser.elementByCss(
|
|
'form[action="/greeting"] button'
|
|
)
|
|
await button.click()
|
|
// Navigation should finish immedately because the page is
|
|
// fully prefetched.
|
|
const greeting = await browser.elementById('greeting')
|
|
expect(await greeting.innerHTML()).toBe('random-greeting [1]')
|
|
}, 'no-requests')
|
|
})
|
|
|
|
it('call router.prefetch(..., {onInvalidate}) after cache is revalidated', async () => {
|
|
// This is the similar to the previous tests, but uses a custom Link
|
|
// implementation that calls router.prefetch manually. It demonstrates it's
|
|
// possible to simulate the revalidating behavior of Link using the manual
|
|
// prefetch API.
|
|
let act: ReturnType<typeof createRouterAct>
|
|
let page: Playwright.Page
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const linkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-manual-prefetch-link-accordion="/greeting"]'
|
|
)
|
|
|
|
// Reveal the link that points to the target page to trigger a prefetch
|
|
await act(
|
|
async () => {
|
|
await linkVisibilityToggle.click()
|
|
},
|
|
{
|
|
includes: 'random-greeting',
|
|
}
|
|
)
|
|
|
|
// Install fake timers — freezes the 300ms cooldown setTimeout
|
|
await page.clock.install()
|
|
|
|
// Perform an action that calls revalidatePath. This should cause the
|
|
// corresponding entry to be evicted from the client cache, and a new
|
|
// prefetch to be requested.
|
|
await act(async () => {
|
|
const revalidateByPath = await browser.elementById('revalidate-by-path')
|
|
await revalidateByPath.click()
|
|
})
|
|
|
|
// Advance past cooldown inside act() to intercept the re-prefetch
|
|
await act(
|
|
async () => {
|
|
await page.clock.fastForward(300)
|
|
},
|
|
{
|
|
includes: 'random-greeting [1]',
|
|
}
|
|
)
|
|
TestLog.assert(['REQUEST: random-greeting'])
|
|
|
|
// Navigate to the target page.
|
|
await act(async () => {
|
|
const link = await browser.elementByCss('a[href="/greeting"]')
|
|
await link.click()
|
|
// Navigation should finish immedately because the page is
|
|
// fully prefetched.
|
|
const greeting = await browser.elementById('greeting')
|
|
expect(await greeting.innerHTML()).toBe('random-greeting [1]')
|
|
}, 'no-requests')
|
|
})
|
|
|
|
it('evict client cache when Server Action calls revalidateTag', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
let page: Playwright.Page
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const linkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-link-accordion="/greeting"]'
|
|
)
|
|
|
|
// Reveal the link the target page to trigger a prefetch.
|
|
await act(
|
|
async () => {
|
|
await linkVisibilityToggle.click()
|
|
},
|
|
{
|
|
includes: 'random-greeting',
|
|
}
|
|
)
|
|
|
|
// Install fake timers — freezes the 300ms cooldown setTimeout
|
|
await page.clock.install()
|
|
|
|
// Perform an action that calls revalidateTag. This should cause the
|
|
// corresponding entry to be evicted from the client cache, and a new
|
|
// prefetch to be requested.
|
|
await act(async () => {
|
|
const revalidateByTag = await browser.elementById('revalidate-by-tag')
|
|
await revalidateByTag.click()
|
|
})
|
|
|
|
// Advance past cooldown inside act() to intercept the re-prefetch
|
|
await act(
|
|
async () => {
|
|
await page.clock.fastForward(300)
|
|
},
|
|
{
|
|
includes: 'random-greeting [1]',
|
|
}
|
|
)
|
|
TestLog.assert(['REQUEST: random-greeting'])
|
|
|
|
// Navigate to the target page.
|
|
await act(async () => {
|
|
const link = await browser.elementByCss('a[href="/greeting"]')
|
|
await link.click()
|
|
// Navigation should finish immedately because the page is
|
|
// fully prefetched.
|
|
const greeting = await browser.elementById('greeting')
|
|
expect(await greeting.innerHTML()).toBe('random-greeting [1]')
|
|
}, 'no-requests')
|
|
})
|
|
|
|
it('re-fetch visible links after a navigation, if needed', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/refetch-on-new-base-tree/a', {
|
|
beforePageLoad(page) {
|
|
act = createRouterAct(page)
|
|
},
|
|
})
|
|
|
|
const linkALinkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-link-accordion="/refetch-on-new-base-tree/a"]'
|
|
)
|
|
const linkBLinkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-link-accordion="/refetch-on-new-base-tree/b"]'
|
|
)
|
|
|
|
// Reveal the links to trigger prefetches
|
|
await act(async () => {
|
|
await linkALinkVisibilityToggle.click()
|
|
await linkBLinkVisibilityToggle.click()
|
|
}, [
|
|
// Page B's content should have been prefetched
|
|
{
|
|
includes: 'Page B content',
|
|
},
|
|
// Page A's content should not be prefetched because we're already on that
|
|
// page. When prefetching with `prefetch='unstable_forceStale'`, we only prefetch the
|
|
// delta between the current route and the target route.
|
|
{
|
|
includes: 'Page A content',
|
|
block: 'reject',
|
|
},
|
|
])
|
|
|
|
// Navigate to page B
|
|
await act(
|
|
async () => {
|
|
const link = await browser.elementByCss(
|
|
'a[href="/refetch-on-new-base-tree/b"]'
|
|
)
|
|
await link.click()
|
|
const content = await browser.elementById('page-b-content')
|
|
expect(await content.innerHTML()).toBe('Page B content')
|
|
},
|
|
// The link for page A is re-prefetched again, even though it's an
|
|
// existing link, because the delta between the current route and the
|
|
// target route has changed.
|
|
//
|
|
// This time, the response does include the content for page A.
|
|
//
|
|
// TODO: The request is actually skipped entirely because <Link
|
|
// prefetch={true} /> now reads from the bfcache before issuing a prefetch
|
|
// request, which wasn't true before the test was written. I'm leaving
|
|
// the test here for now, though, since we may want to re-write it in
|
|
// terms of runtime prefetching at some point. There's other coverage of
|
|
// this behavior though so it might be fine to just remove the whole test.
|
|
// {
|
|
// includes: 'Page A content',
|
|
// }
|
|
'no-requests'
|
|
)
|
|
|
|
// Navigate to page A
|
|
await act(
|
|
async () => {
|
|
const link = await browser.elementByCss(
|
|
'a[href="/refetch-on-new-base-tree/a"]'
|
|
)
|
|
await link.click()
|
|
const content = await browser.elementById('page-a-content')
|
|
expect(await content.innerHTML()).toBe('Page A content')
|
|
},
|
|
// There should be no new requests because everything is fully prefetched.
|
|
'no-requests'
|
|
)
|
|
})
|
|
|
|
it('delay re-prefetch after revalidation to allow CDN propagation', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
let page: Playwright.Page
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const linkVisibilityToggle = await browser.elementByCss(
|
|
'input[data-link-accordion="/greeting"]'
|
|
)
|
|
|
|
// Reveal the link the target page to trigger a prefetch
|
|
await act(
|
|
async () => {
|
|
await linkVisibilityToggle.click()
|
|
},
|
|
{
|
|
includes: 'random-greeting',
|
|
}
|
|
)
|
|
|
|
// Install fake timers so the 300ms cooldown setTimeout in the
|
|
// browser is frozen until we explicitly advance the clock.
|
|
await page.clock.install()
|
|
|
|
// Perform an action that calls revalidatePath. This triggers a 300ms
|
|
// cooldown before any new prefetch requests can be made.
|
|
await act(async () => {
|
|
const revalidateByPath = await browser.elementById('revalidate-by-path')
|
|
await revalidateByPath.click()
|
|
})
|
|
|
|
// The cooldown timer is frozen, so no prefetch should have occurred.
|
|
TestLog.assert([])
|
|
|
|
// Advance partway through the cooldown — still no prefetch.
|
|
await page.clock.fastForward(150)
|
|
TestLog.assert([])
|
|
|
|
// Advance past the cooldown (300ms total). This fires the cooldown
|
|
// callback, which triggers a re-prefetch. Use act() to intercept
|
|
// the prefetch request and ensure the response is fully delivered
|
|
// to the browser.
|
|
await act(
|
|
async () => {
|
|
await page.clock.fastForward(150)
|
|
},
|
|
{
|
|
includes: 'random-greeting [1]',
|
|
}
|
|
)
|
|
TestLog.assert(['REQUEST: random-greeting'])
|
|
|
|
// Navigate to the target page.
|
|
await act(async () => {
|
|
const link = await browser.elementByCss('a[href="/greeting"]')
|
|
await link.click()
|
|
// Navigation should finish immediately because the page is
|
|
// fully prefetched.
|
|
const greeting = await browser.elementById('greeting')
|
|
expect(await greeting.innerHTML()).toBe('random-greeting [1]')
|
|
}, 'no-requests')
|
|
})
|
|
})
|