|
| 1 | +/** |
| 2 | + * Feed ISR Caching e2e tests (unauthenticated users) |
| 3 | + * |
| 4 | + * Architecture overview: |
| 5 | + * - Unauthenticated users are routed by proxy.ts to /[locale]/feeds/[type]/[id]/static/ |
| 6 | + * - That route uses `force-static` + `revalidate: 1209600` (14 days) |
| 7 | + * - On the first visit, Next.js renders the page and caches it |
| 8 | + * - On subsequent visits, Next.js serves the cached HTML without re-rendering |
| 9 | + * |
| 10 | + * How we detect cache hits/misses: |
| 11 | + * - Next.js sets the `x-nextjs-cache` response header on ISR routes: |
| 12 | + * MISS → page was freshly rendered (first visit or after revalidation) |
| 13 | + * HIT → page was served from the ISR cache |
| 14 | + * STALE → page was served from stale cache while revalidation runs in background |
| 15 | + * - We intercept the browser's GET request to the feed page and inspect this header. |
| 16 | + * |
| 17 | + */ |
| 18 | + |
| 19 | +export {}; |
| 20 | + |
| 21 | +const TEST_FEED_ID = 'test-516'; |
| 22 | +const TEST_FEED_DATA_TYPE = 'gtfs'; |
| 23 | +const FEED_URL = `/feeds/${TEST_FEED_DATA_TYPE}/${TEST_FEED_ID}`; |
| 24 | + |
| 25 | +/** |
| 26 | + * Calls the /api/revalidate endpoint to bust the ISR cache for the test feed. |
| 27 | + * This simulates what happens when the backend triggers a revalidation webhook |
| 28 | + * (e.g. after a feed update), which in production invalidates the cached page. |
| 29 | + * |
| 30 | + * The REVALIDATE_SECRET must match the value set in the Next.js server's env. |
| 31 | + * It is read from Cypress env (loaded from .env.development via cypress.config.ts). |
| 32 | + */ |
| 33 | +function revalidateTestFeed(): void { |
| 34 | + const secret = Cypress.env('REVALIDATE_SECRET') as string; |
| 35 | + cy.request({ |
| 36 | + method: 'POST', |
| 37 | + url: '/api/revalidate', |
| 38 | + headers: { |
| 39 | + 'x-revalidate-secret': secret, |
| 40 | + 'content-type': 'application/json', |
| 41 | + }, |
| 42 | + body: { |
| 43 | + type: 'specific-feeds', |
| 44 | + feedIds: [TEST_FEED_ID], |
| 45 | + }, |
| 46 | + }) |
| 47 | + .its('status') |
| 48 | + .should('eq', 200); |
| 49 | +} |
| 50 | + |
| 51 | +describe('Feed ISR Caching - Unauthenticated', () => { |
| 52 | + /** |
| 53 | + * Ensure the ISR cache is busted before the suite runs so we always |
| 54 | + * start from a known MISS state, regardless of prior test runs. |
| 55 | + */ |
| 56 | + before(() => { |
| 57 | + revalidateTestFeed(); |
| 58 | + }); |
| 59 | + |
| 60 | + describe('First visit (cache MISS)', () => { |
| 61 | + it('should render the page dynamically on the first visit', () => { |
| 62 | + // Intercept the page request and capture the x-nextjs-cache header. |
| 63 | + // The alias lets us assert on the response after cy.visit() resolves. |
| 64 | + cy.intercept('GET', FEED_URL).as('feedPageRequest'); |
| 65 | + |
| 66 | + cy.visit(FEED_URL, { timeout: 30000 }); |
| 67 | + |
| 68 | + // Wait for the page request and assert the cache header is MISS. |
| 69 | + // On the very first visit (or after revalidation), Next.js renders |
| 70 | + // the page fresh and populates the ISR cache. |
| 71 | + cy.wait('@feedPageRequest') |
| 72 | + .its('response.headers.x-nextjs-cache') |
| 73 | + // MISS means the page was freshly rendered (not served from cache). |
| 74 | + // STALE is also acceptable here if a prior cached version existed but |
| 75 | + // was invalidated — Next.js serves stale while revalidating in background. |
| 76 | + .should('be.oneOf', ['MISS', 'STALE']); |
| 77 | + |
| 78 | + // Sanity check: the page content is actually rendered |
| 79 | + cy.get('[data-testid="feed-provider"]', { timeout: 10000 }).should( |
| 80 | + 'contain', |
| 81 | + 'Metropolitan Transit Authority (MTA)', |
| 82 | + ); |
| 83 | + }); |
| 84 | + }); |
| 85 | + |
| 86 | + describe('Second visit (cache HIT)', () => { |
| 87 | + it('should serve the page from the ISR cache on a revisit', () => { |
| 88 | + // Intercept the page request again for the second visit. |
| 89 | + cy.intercept('GET', FEED_URL).as('feedPageCacheHit'); |
| 90 | + |
| 91 | + // Visit the same URL again — Next.js should now serve from ISR cache. |
| 92 | + cy.visit(FEED_URL, { timeout: 30000 }); |
| 93 | + |
| 94 | + cy.wait('@feedPageCacheHit') |
| 95 | + .its('response.headers.x-nextjs-cache') |
| 96 | + // HIT means the page was served from the ISR cache without re-rendering. |
| 97 | + .should('eq', 'HIT'); |
| 98 | + |
| 99 | + // Content should still be correct when served from cache |
| 100 | + cy.get('[data-testid="feed-provider"]', { timeout: 10000 }).should( |
| 101 | + 'contain', |
| 102 | + 'Metropolitan Transit Authority (MTA)', |
| 103 | + ); |
| 104 | + }); |
| 105 | + }); |
| 106 | + |
| 107 | + describe('After revalidation (cache MISS again)', () => { |
| 108 | + it('should bust the ISR cache when the revalidate endpoint is called', () => { |
| 109 | + // First, confirm the page is currently cached (HIT) before we bust it. |
| 110 | + cy.intercept('GET', FEED_URL).as('feedPageBeforeRevalidate'); |
| 111 | + cy.visit(FEED_URL, { timeout: 30000 }); |
| 112 | + cy.wait('@feedPageBeforeRevalidate') |
| 113 | + .its('response.headers.x-nextjs-cache') |
| 114 | + .should('eq', 'HIT'); |
| 115 | + |
| 116 | + // Trigger cache invalidation via the revalidate API endpoint. |
| 117 | + // This simulates a backend webhook call after a feed update. |
| 118 | + revalidateTestFeed(); |
| 119 | + |
| 120 | + // Visit the page again — the cache was busted, so Next.js should |
| 121 | + // re-render the page (MISS or STALE). |
| 122 | + cy.intercept('GET', FEED_URL).as('feedPageAfterRevalidate'); |
| 123 | + cy.visit(FEED_URL, { timeout: 30000 }); |
| 124 | + |
| 125 | + cy.wait('@feedPageAfterRevalidate') |
| 126 | + .its('response.headers.x-nextjs-cache') |
| 127 | + // After revalidation, the cache is invalidated. Next.js will either: |
| 128 | + // - MISS: render fresh immediately |
| 129 | + // - STALE: serve the old cache while re-rendering in background |
| 130 | + // Either way, the cache was busted — a HIT here would be a failure. |
| 131 | + .should('be.oneOf', ['MISS', 'STALE']); |
| 132 | + |
| 133 | + // Content should still be correct after revalidation |
| 134 | + cy.get('[data-testid="feed-provider"]', { timeout: 10000 }).should( |
| 135 | + 'contain', |
| 136 | + 'Metropolitan Transit Authority (MTA)', |
| 137 | + ); |
| 138 | + }); |
| 139 | + }); |
| 140 | +}); |
0 commit comments