From b8f52080d059a0c1c6de16b9d78dd2cf1670d1c2 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 18 Jun 2024 12:55:40 +0200 Subject: [PATCH] test: refactor app router on-demand revalidation test and add test cases for not prerendered content --- tests/e2e/on-demand-app.test.ts | 343 ++++++------------ .../api/on-demand-revalidate/path/route.ts | 5 +- .../app/api/on-demand-revalidate/tag/route.ts | 5 +- 3 files changed, 122 insertions(+), 231 deletions(-) diff --git a/tests/e2e/on-demand-app.test.ts b/tests/e2e/on-demand-app.test.ts index 47f411df4a..fcf971e2f4 100644 --- a/tests/e2e/on-demand-app.test.ts +++ b/tests/e2e/on-demand-app.test.ts @@ -2,47 +2,75 @@ import { expect } from '@playwright/test' import { test } from '../utils/playwright-helpers.js' test.describe('app router on-demand revalidation', () => { - test('revalidatePath', async ({ page, pollUntilHeadersMatch, serverComponents }) => { - // in case there is retry or some other test did hit that path before - // we want to make sure that cdn cache is not warmed up - const purgeCdnCache = await page.goto( - new URL('/api/purge-cdn?path=/static-fetch/1', serverComponents.url).href, - ) - expect(purgeCdnCache?.status()).toBe(200) - - // wait a bit until cdn cache purge propagates - await page.waitForTimeout(500) - - const response1 = await pollUntilHeadersMatch( - new URL('static-fetch/1', serverComponents.url).href, - { + for (const { label, prerendered, pagePath, revalidateApiPath, expectedH1Content } of [ + { + label: 'revalidatePath (prerendered page)', + prerendered: true, + pagePath: '/static-fetch/1', + revalidateApiPath: '/api/on-demand-revalidate/path?path=/static-fetch/1', + expectedH1Content: 'Hello, Statically fetched show 1', + }, + { + label: 'revalidateTag (prerendered page)', + prerendered: true, + pagePath: '/static-fetch/2', + revalidateApiPath: '/api/on-demand-revalidate/tag?tag=show-2', + expectedH1Content: 'Hello, Statically fetched show 2', + }, + { + label: 'revalidatePath (not prerendered page)', + prerendered: false, + pagePath: '/static-fetch/3', + revalidateApiPath: '/api/on-demand-revalidate/path?path=/static-fetch/3', + expectedH1Content: 'Hello, Statically fetched show 3', + }, + { + label: 'revalidateTag (not prerendered page)', + prerendered: false, + pagePath: '/static-fetch/4', + revalidateApiPath: '/api/on-demand-revalidate/tag?tag=show-4', + expectedH1Content: 'Hello, Statically fetched show 4', + }, + ]) { + test(label, async ({ page, pollUntilHeadersMatch, serverComponents }) => { + // in case there is retry or some other test did hit that path before + // we want to make sure that cdn cache is not warmed up + const purgeCdnCache = await page.goto( + new URL(`/api/purge-cdn?path=${pagePath}`, serverComponents.url).href, + ) + expect(purgeCdnCache?.status()).toBe(200) + + // wait a bit until cdn cache purge propagates + await page.waitForTimeout(500) + + const response1 = await pollUntilHeadersMatch(new URL(pagePath, serverComponents.url).href, { headersToMatch: { // either first time hitting this route or we invalidated // just CDN node in earlier step // we will invoke function and see Next cache hit status \ - // in the response because it was prerendered at build time + // in the response if it was prerendered at build time // or regenerated in previous attempt to run this test - 'cache-status': [/"Netlify Edge"; fwd=(miss|stale)/m, /"Next.js"; hit/m], + 'cache-status': [ + /"Netlify Edge"; fwd=(miss|stale)/m, + prerendered ? /"Next.js"; hit/m : /"Next.js"; (hit|fwd=miss)/m, + ], }, headersNotMatchedMessage: 'First request to tested page should be a miss or stale on the Edge and hit in Next.js', - }, - ) - const headers1 = response1?.headers() || {} - expect(response1?.status()).toBe(200) - expect(headers1['x-nextjs-cache']).toBeUndefined() - expect(headers1['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) + }) + const headers1 = response1?.headers() || {} + expect(response1?.status()).toBe(200) + expect(headers1['x-nextjs-cache']).toBeUndefined() + expect(headers1['netlify-cdn-cache-control']).toBe( + 's-maxage=31536000, stale-while-revalidate=31536000', + ) - const date1 = await page.textContent('[data-testid="date-now"]') + const date1 = await page.textContent('[data-testid="date-now"]') - const h1 = await page.textContent('h1') - expect(h1).toBe('Hello, Statically fetched show 1') + const h1 = await page.textContent('h1') + expect(h1).toBe(expectedH1Content) - const response2 = await pollUntilHeadersMatch( - new URL('static-fetch/1', serverComponents.url).href, - { + const response2 = await pollUntilHeadersMatch(new URL(pagePath, serverComponents.url).href, { headersToMatch: { // we are hitting the same page again and we most likely will see // CDN hit (in this case Next reported cache status is omitted @@ -52,36 +80,31 @@ test.describe('app router on-demand revalidation', () => { }, headersNotMatchedMessage: 'Second request to tested page should most likely be a hit on the Edge (optionally miss or stale if different CDN node)', - }, - ) - const headers2 = response2?.headers() || {} - expect(response2?.status()).toBe(200) - expect(headers2['x-nextjs-cache']).toBeUndefined() - if (!headers2['cache-status'].includes('"Netlify Edge"; hit')) { - // if we missed CDN cache, we will see Next cache hit status - // as we reuse cached response - expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m) - } - expect(headers2['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page is cached - const date2 = await page.textContent('[data-testid="date-now"]') - expect(date2).toBe(date1) - - const revalidate = await page.goto( - new URL('/api/on-demand-revalidate/path', serverComponents.url).href, - ) - expect(revalidate?.status()).toBe(200) - - // wait a bit until cdn tags and invalidated and cdn is purged - await page.waitForTimeout(500) - - // now after the revalidation it should have a different date - const response3 = await pollUntilHeadersMatch( - new URL('static-fetch/1', serverComponents.url).href, - { + }) + const headers2 = response2?.headers() || {} + expect(response2?.status()).toBe(200) + expect(headers2['x-nextjs-cache']).toBeUndefined() + if (!headers2['cache-status'].includes('"Netlify Edge"; hit')) { + // if we missed CDN cache, we will see Next cache hit status + // as we reuse cached response + expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m) + } + expect(headers2['netlify-cdn-cache-control']).toBe( + 's-maxage=31536000, stale-while-revalidate=31536000', + ) + + // the page is cached + const date2 = await page.textContent('[data-testid="date-now"]') + expect(date2).toBe(date1) + + const revalidate = await page.goto(new URL(revalidateApiPath, serverComponents.url).href) + expect(revalidate?.status()).toBe(200) + + // wait a bit until cdn tags and invalidated and cdn is purged + await page.waitForTimeout(500) + + // now after the revalidation it should have a different date + const response3 = await pollUntilHeadersMatch(new URL(pagePath, serverComponents.url).href, { headersToMatch: { // revalidatePath just marks the page(s) as invalid and does NOT // automatically refreshes the cache. This request will cause @@ -92,156 +115,19 @@ test.describe('app router on-demand revalidation', () => { }, headersNotMatchedMessage: 'Third request to tested page should be a miss or stale on the Edge and miss in Next.js after on-demand revalidation', - }, - ) - const headers3 = response3?.headers() || {} - expect(response3?.status()).toBe(200) - expect(headers3?.['x-nextjs-cache']).toBeUndefined() - expect(headers3['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page has now an updated date - const date3 = await page.textContent('[data-testid="date-now"]') - expect(date3).not.toBe(date2) - - const response4 = await pollUntilHeadersMatch( - new URL('static-fetch/1', serverComponents.url).href, - { - headersToMatch: { - // we are hitting the same page again and we most likely will see - // CDN hit (in this case Next reported cache status is omitted - // as it didn't actually take place in handling this request) - // or we will see CDN miss because different CDN node handled request - 'cache-status': /"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m, - }, - headersNotMatchedMessage: - 'Fourth request to tested page should most likely be a hit on the Edge (optionally miss or stale if different CDN node)', - }, - ) - const headers4 = response4?.headers() || {} - expect(response4?.status()).toBe(200) - expect(headers4?.['x-nextjs-cache']).toBeUndefined() - if (!headers4['cache-status'].includes('"Netlify Edge"; hit')) { - // if we missed CDN cache, we will see Next cache hit status - // as we reuse cached response - expect(headers4['cache-status']).toMatch(/"Next.js"; hit/m) - } - expect(headers4['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page is cached - const date4 = await page.textContent('[data-testid="date-now"]') - expect(date4).toBe(date3) - }) - - test('revalidateTag', async ({ page, pollUntilHeadersMatch, serverComponents }) => { - // in case there is retry or some other test did hit that path before - // we want to make sure that cdn cache is not warmed up - const purgeCdnCache = await page.goto( - new URL('/api/purge-cdn?path=/static-fetch-1', serverComponents.url).href, - ) - expect(purgeCdnCache?.status()).toBe(200) - - // wait a bit until cdn cache purge propagates - await page.waitForTimeout(500) - - const response1 = await pollUntilHeadersMatch( - new URL('static-fetch-1', serverComponents.url).href, - { - headersToMatch: { - // either first time hitting this route or we invalidated - // just CDN node in earlier step - // we will invoke function and see Next cache hit status \ - // in the response because it was prerendered at build time - // or regenerated in previous attempt to run this test - 'cache-status': [/"Netlify Edge"; fwd=(miss|stale)/m, /"Next.js"; hit/m], - }, - headersNotMatchedMessage: - 'First request to tested page should be a miss or stale on the Edge and hit in Next.js', - }, - ) - const headers1 = response1?.headers() || {} - expect(response1?.status()).toBe(200) - expect(headers1['x-nextjs-cache']).toBeUndefined() - expect(headers1['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - const date1 = await page.textContent('[data-testid="date-now"]') - - const h1 = await page.textContent('h1') - expect(h1).toBe('Hello, Static Fetch 1') - - const response2 = await pollUntilHeadersMatch( - new URL('static-fetch-1', serverComponents.url).href, - { - headersToMatch: { - // we are hitting the same page again and we most likely will see - // CDN hit (in this case Next reported cache status is omitted - // as it didn't actually take place in handling this request) - // or we will see CDN miss because different CDN node handled request - 'cache-status': /"Netlify Edge"; (hit|fwd=miss|fwd=stale)/m, - }, - headersNotMatchedMessage: - 'Second request to tested page should most likely be a hit on the Edge (optionally miss or stale if different CDN node)', - }, - ) - const headers2 = response2?.headers() || {} - expect(response2?.status()).toBe(200) - expect(headers2['x-nextjs-cache']).toBeUndefined() - if (!headers2['cache-status'].includes('"Netlify Edge"; hit')) { - // if we missed CDN cache, we will see Next cache hit status - // as we reuse cached response - expect(headers2['cache-status']).toMatch(/"Next.js"; hit/m) - } - expect(headers2['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page is cached - const date2 = await page.textContent('[data-testid="date-now"]') - expect(date2).toBe(date1) - - const revalidate = await page.goto( - new URL('/api/on-demand-revalidate/tag', serverComponents.url).href, - ) - expect(revalidate?.status()).toBe(200) - - // wait a bit until cdn tags and invalidated and cdn is purged - await page.waitForTimeout(500) - - // now after the revalidation it should have a different date - const response3 = await pollUntilHeadersMatch( - new URL('static-fetch-1', serverComponents.url).href, - { - headersToMatch: { - // revalidateTag just marks the page(s) as invalid and does NOT - // automatically refreshes the cache. This request will cause - // Next.js cache miss and new response will be generated and cached - // Depending if we hit same CDN node as previous request, we might - // get either fwd=miss or fwd=stale - 'cache-status': [/"Next.js"; fwd=miss/m, /"Netlify Edge"; fwd=(miss|stale)/m], - }, - headersNotMatchedMessage: - 'Third request to tested page should be a miss or stale on the Edge and miss in Next.js after on-demand revalidation', - }, - ) - const headers3 = response3?.headers() || {} - expect(response3?.status()).toBe(200) - expect(headers3?.['x-nextjs-cache']).toBeUndefined() - expect(headers3['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page has now an updated date - const date3 = await page.textContent('[data-testid="date-now"]') - expect(date3).not.toBe(date2) - - const response4 = await pollUntilHeadersMatch( - new URL('static-fetch-1', serverComponents.url).href, - { + }) + const headers3 = response3?.headers() || {} + expect(response3?.status()).toBe(200) + expect(headers3?.['x-nextjs-cache']).toBeUndefined() + expect(headers3['netlify-cdn-cache-control']).toBe( + 's-maxage=31536000, stale-while-revalidate=31536000', + ) + + // the page has now an updated date + const date3 = await page.textContent('[data-testid="date-now"]') + expect(date3).not.toBe(date2) + + const response4 = await pollUntilHeadersMatch(new URL(pagePath, serverComponents.url).href, { headersToMatch: { // we are hitting the same page again and we most likely will see // CDN hit (in this case Next reported cache status is omitted @@ -251,23 +137,22 @@ test.describe('app router on-demand revalidation', () => { }, headersNotMatchedMessage: 'Fourth request to tested page should most likely be a hit on the Edge (optionally miss or stale if different CDN node)', - }, - ) - const headers4 = response4?.headers() || {} - expect(response4?.status()).toBe(200) - expect(headers4?.['x-nextjs-cache']).toBeUndefined() - - if (!headers4['cache-status'].includes('"Netlify Edge"; hit')) { - // if we missed CDN cache, we will see Next cache hit status - // as we reuse cached response - expect(headers4['cache-status']).toMatch(/"Next.js"; hit/m) - } - expect(headers4['netlify-cdn-cache-control']).toBe( - 's-maxage=31536000, stale-while-revalidate=31536000', - ) - - // the page is cached - const date4 = await page.textContent('[data-testid="date-now"]') - expect(date4).toBe(date3) - }) + }) + const headers4 = response4?.headers() || {} + expect(response4?.status()).toBe(200) + expect(headers4?.['x-nextjs-cache']).toBeUndefined() + if (!headers4['cache-status'].includes('"Netlify Edge"; hit')) { + // if we missed CDN cache, we will see Next cache hit status + // as we reuse cached response + expect(headers4['cache-status']).toMatch(/"Next.js"; hit/m) + } + expect(headers4['netlify-cdn-cache-control']).toBe( + 's-maxage=31536000, stale-while-revalidate=31536000', + ) + + // the page is cached + const date4 = await page.textContent('[data-testid="date-now"]') + expect(date4).toBe(date3) + }) + } }) diff --git a/tests/fixtures/server-components/app/api/on-demand-revalidate/path/route.ts b/tests/fixtures/server-components/app/api/on-demand-revalidate/path/route.ts index 249b132235..4da6b30321 100644 --- a/tests/fixtures/server-components/app/api/on-demand-revalidate/path/route.ts +++ b/tests/fixtures/server-components/app/api/on-demand-revalidate/path/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from 'next/server' import { revalidatePath } from 'next/cache' export async function GET(request: NextRequest) { - revalidatePath('/static-fetch/[id]', 'page') + const url = new URL(request.url) + const pathToRevalidate = url.searchParams.get('path') ?? '/static-fetch/[id]/page' + + revalidatePath(pathToRevalidate) return NextResponse.json({ revalidated: true, now: new Date().toISOString() }) } diff --git a/tests/fixtures/server-components/app/api/on-demand-revalidate/tag/route.ts b/tests/fixtures/server-components/app/api/on-demand-revalidate/tag/route.ts index 613b951b00..ae91b90b4e 100644 --- a/tests/fixtures/server-components/app/api/on-demand-revalidate/tag/route.ts +++ b/tests/fixtures/server-components/app/api/on-demand-revalidate/tag/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from 'next/server' import { revalidateTag } from 'next/cache' export async function GET(request: NextRequest) { - revalidateTag('collection') + const url = new URL(request.url) + const tagToRevalidate = url.searchParams.get('tag') ?? 'collection' + + revalidateTag(tagToRevalidate) return NextResponse.json({ revalidated: true, now: new Date().toISOString() }) }