|
1 | 1 | import { nextTestSetup } from 'e2e-utils' |
| 2 | +import { retry } from 'next-test-utils' |
| 3 | +import type { Playwright } from '../../../lib/next-webdriver' |
| 4 | +import { styleText } from 'node:util' |
2 | 5 |
|
3 | 6 | describe('cache-components-dev-warmup', () => { |
4 | | - const { next } = nextTestSetup({ |
| 7 | + const { next, isTurbopack } = nextTestSetup({ |
5 | 8 | files: __dirname, |
6 | 9 | }) |
7 | 10 |
|
| 11 | + // Restart the dev server for each test to clear the in-memory cache. |
| 12 | + // We're testing cache-warming behavior here, so we don't want tests to interfere with each other. |
| 13 | + let isFirstTest = true |
| 14 | + beforeEach(async () => { |
| 15 | + if (isFirstTest) { |
| 16 | + // There's no point restarting if this is the first test. |
| 17 | + isFirstTest = false |
| 18 | + return |
| 19 | + } |
| 20 | + |
| 21 | + await next.stop() |
| 22 | + await next.clean() |
| 23 | + await next.start() |
| 24 | + }) |
| 25 | + |
8 | 26 | function assertLog( |
9 | 27 | logs: Array<{ source: string; message: string }>, |
10 | 28 | message: string, |
@@ -38,145 +56,206 @@ describe('cache-components-dev-warmup', () => { |
38 | 56 | ]) |
39 | 57 | } |
40 | 58 |
|
41 | | - describe('logs with Prerender or Server environment depending based on whether the timing of when the log runs relative to this environment boundary', () => { |
42 | | - it('cached data + cached fetch', async () => { |
43 | | - const path = '/simple' |
44 | | - const browser = await next.browser(path) |
45 | | - |
46 | | - const assertLogs = async () => { |
47 | | - const logs = await browser.log() |
48 | | - assertLog(logs, 'after cache read - layout', 'Prerender') |
49 | | - assertLog(logs, 'after cache read - page', 'Prerender') |
50 | | - assertLog(logs, 'after successive cache reads - page', 'Prerender') |
51 | | - assertLog(logs, 'after cached fetch - layout', 'Prerender') |
52 | | - assertLog(logs, 'after cached fetch - page', 'Prerender') |
53 | | - |
54 | | - assertLog(logs, 'after uncached fetch - layout', 'Server') |
55 | | - assertLog(logs, 'after uncached fetch - page', 'Server') |
56 | | - } |
57 | | - |
58 | | - // Initial load. |
59 | | - await assertLogs() |
60 | | - |
61 | | - // After another load (with warm caches) the logs should be the same. |
62 | | - await browser.loadPage(next.url + path) // clears old logs |
63 | | - await assertLogs() |
64 | | - |
65 | | - // After a revalidation the subsequent warmup render must discard stale |
66 | | - // cache entries. |
67 | | - // This should not affect the environment labels. |
68 | | - await next.fetch(`/revalidate?path=${encodeURIComponent(path)}`) |
| 59 | + async function testInitialLoad( |
| 60 | + path: string, |
| 61 | + assertLogs: (browser: Playwright) => Promise<void> |
| 62 | + ) { |
| 63 | + const browser = await next.browser(path) |
69 | 64 |
|
70 | | - await browser.loadPage(next.url + path) // clears old logs |
71 | | - await assertLogs() |
72 | | - }) |
| 65 | + // Initial load. |
| 66 | + await retry(() => assertLogs(browser)) |
73 | 67 |
|
74 | | - it('cached data + private cache', async () => { |
75 | | - const path = '/private-cache' |
76 | | - const browser = await next.browser(path) |
| 68 | + // After another load (with warm caches) the logs should be the same. |
| 69 | + await browser.loadPage(next.url + path) // clears old logs |
| 70 | + await retry(() => assertLogs(browser)) |
| 71 | + |
| 72 | + if (isTurbopack) { |
| 73 | + // FIXME: |
| 74 | + // In Turbopack, requests to the /revalidate route seem to occasionally crash |
| 75 | + // due to some HMR or compilation issue. `revalidatePath` throws this error: |
| 76 | + // |
| 77 | + // Invariant: static generation store missing in revalidatePath <path> |
| 78 | + // |
| 79 | + // This is unrelated to the logic being tested here, so for now, we skip the assertions |
| 80 | + // that require us to revalidate. |
| 81 | + console.log( |
| 82 | + styleText( |
| 83 | + 'red', |
| 84 | + 'WARNING: skipping revalidation assertions in turbopack' |
| 85 | + ) |
| 86 | + ) |
| 87 | + return |
| 88 | + } |
77 | 89 |
|
78 | | - const assertLogs = async () => { |
79 | | - const logs = await browser.log() |
80 | | - assertLog(logs, 'after cache read - layout', 'Prerender') |
81 | | - assertLog(logs, 'after cache read - page', 'Prerender') |
| 90 | + // After a revalidation the subsequent warmup render must discard stale |
| 91 | + // cache entries. |
| 92 | + // This should not affect the environment labels. |
| 93 | + await revalidatePath(path) |
82 | 94 |
|
83 | | - // Private caches are dynamic holes in static prerenders, |
84 | | - // so they shouldn't resolve in the static stage. |
85 | | - assertLog(logs, 'after private cache read - page', 'Server') // TODO: 'Runtime Prerender' |
86 | | - assertLog(logs, 'after private cache read - layout', 'Server') // TODO: 'Runtime Prerender' |
87 | | - assertLog(logs, 'after successive private cache reads - page', 'Server') // TODO: 'Runtime Prerender' |
| 95 | + await browser.loadPage(next.url + path) // clears old logs |
| 96 | + await retry(() => assertLogs(browser)) |
| 97 | + } |
88 | 98 |
|
89 | | - assertLog(logs, 'after uncached fetch - layout', 'Server') |
90 | | - assertLog(logs, 'after uncached fetch - page', 'Server') |
91 | | - } |
| 99 | + async function testNavigation( |
| 100 | + path: string, |
| 101 | + assertLogs: (browser: Playwright) => Promise<void> |
| 102 | + ) { |
| 103 | + const browser = await next.browser('/') |
| 104 | + |
| 105 | + // Initial nav (first time loading the page) |
| 106 | + await browser.elementByCss(`a[href="${path}"]`).click() |
| 107 | + await retry(() => assertLogs(browser)) |
| 108 | + |
| 109 | + // Reload, and perform another nav (with warm caches). the logs should be the same. |
| 110 | + await browser.loadPage(next.url + '/') // clears old logs |
| 111 | + await browser.elementByCss(`a[href="${path}"]`).click() |
| 112 | + await retry(() => assertLogs(browser)) |
| 113 | + |
| 114 | + if (isTurbopack) { |
| 115 | + // FIXME: |
| 116 | + // In Turbopack, requests to the /revalidate route seem to occasionally crash |
| 117 | + // due to some HMR or compilation issue. `revalidatePath` throws this error: |
| 118 | + // |
| 119 | + // Invariant: static generation store missing in revalidatePath <path> |
| 120 | + // |
| 121 | + // This is unrelated to the logic being tested here, so for now, we skip the assertions |
| 122 | + // that require us to revalidate. |
| 123 | + console.log( |
| 124 | + styleText( |
| 125 | + 'red', |
| 126 | + 'WARNING: skipping revalidation assertions in turbopack' |
| 127 | + ) |
| 128 | + ) |
| 129 | + return |
| 130 | + } |
92 | 131 |
|
93 | | - // Initial load. |
94 | | - await assertLogs() |
| 132 | + // After a revalidation the subsequent warmup render must discard stale |
| 133 | + // cache entries. |
| 134 | + // This should not affect the environment labels. |
| 135 | + await revalidatePath(path) |
95 | 136 |
|
96 | | - // After another load (with warm caches) the logs should be the same. |
97 | | - // Note that private caches are not currently persisted outside of the request that uses them. |
98 | | - await browser.loadPage(next.url + path) // clears old logs |
99 | | - await assertLogs() |
| 137 | + await browser.loadPage(next.url + '/') // clears old logs |
| 138 | + await browser.elementByCss(`a[href="${path}"]`).click() |
| 139 | + await retry(() => assertLogs(browser)) |
| 140 | + } |
100 | 141 |
|
101 | | - // After a revalidation the subsequent warmup render must discard stale |
102 | | - // cache entries. |
103 | | - // This should not affect the environment labels. |
104 | | - await next.fetch(`/revalidate?path=${encodeURIComponent(path)}`) |
| 142 | + async function revalidatePath(path: string) { |
| 143 | + const response = await next.fetch( |
| 144 | + `/revalidate?path=${encodeURIComponent(path)}` |
| 145 | + ) |
| 146 | + if (!response.ok) { |
| 147 | + throw new Error( |
| 148 | + `Failed to revalidate path: '${path}' - server responded with status ${response.status}` |
| 149 | + ) |
| 150 | + } |
| 151 | + } |
105 | 152 |
|
106 | | - await browser.loadPage(next.url + path) // clears old logs |
107 | | - await assertLogs() |
| 153 | + describe.each([ |
| 154 | + { description: 'initial load', isInitialLoad: true }, |
| 155 | + { description: 'navigation', isInitialLoad: false }, |
| 156 | + ])('$description', ({ isInitialLoad }) => { |
| 157 | + describe('cached data resolves in the correct phase', () => { |
| 158 | + it('cached data + cached fetch', async () => { |
| 159 | + const path = '/simple' |
| 160 | + const assertLogs = async (browser: Playwright) => { |
| 161 | + const logs = await browser.log() |
| 162 | + assertLog(logs, 'after cache read - layout', 'Prerender') |
| 163 | + assertLog(logs, 'after cache read - page', 'Prerender') |
| 164 | + assertLog(logs, 'after successive cache reads - page', 'Prerender') |
| 165 | + assertLog(logs, 'after cached fetch - layout', 'Prerender') |
| 166 | + assertLog(logs, 'after cached fetch - page', 'Prerender') |
| 167 | + |
| 168 | + assertLog(logs, 'after uncached fetch - layout', 'Server') |
| 169 | + assertLog(logs, 'after uncached fetch - page', 'Server') |
| 170 | + } |
| 171 | + |
| 172 | + if (isInitialLoad) { |
| 173 | + await testInitialLoad(path, assertLogs) |
| 174 | + } else { |
| 175 | + await testNavigation(path, assertLogs) |
| 176 | + } |
| 177 | + }) |
| 178 | + |
| 179 | + it('cached data + private cache', async () => { |
| 180 | + const path = '/private-cache' |
| 181 | + |
| 182 | + const assertLogs = async (browser: Playwright) => { |
| 183 | + const logs = await browser.log() |
| 184 | + assertLog(logs, 'after cache read - layout', 'Prerender') |
| 185 | + assertLog(logs, 'after cache read - page', 'Prerender') |
| 186 | + |
| 187 | + // Private caches are dynamic holes in static prerenders, |
| 188 | + // so they shouldn't resolve in the static stage. |
| 189 | + assertLog(logs, 'after private cache read - page', 'Server') // TODO: 'Runtime Prerender' |
| 190 | + assertLog(logs, 'after private cache read - layout', 'Server') // TODO: 'Runtime Prerender' |
| 191 | + assertLog( |
| 192 | + logs, |
| 193 | + 'after successive private cache reads - page', |
| 194 | + 'Server' |
| 195 | + ) // TODO: 'Runtime Prerender' |
| 196 | + |
| 197 | + assertLog(logs, 'after uncached fetch - layout', 'Server') |
| 198 | + assertLog(logs, 'after uncached fetch - page', 'Server') |
| 199 | + } |
| 200 | + |
| 201 | + if (isInitialLoad) { |
| 202 | + await testInitialLoad(path, assertLogs) |
| 203 | + } else { |
| 204 | + await testNavigation(path, assertLogs) |
| 205 | + } |
| 206 | + }) |
| 207 | + |
| 208 | + it('cached data + short-lived cached data', async () => { |
| 209 | + const path = '/short-lived-cache' |
| 210 | + |
| 211 | + const assertLogs = async (browser: Playwright) => { |
| 212 | + const logs = await browser.log() |
| 213 | + assertLog(logs, 'after cache read - layout', 'Prerender') |
| 214 | + assertLog(logs, 'after cache read - page', 'Prerender') |
| 215 | + |
| 216 | + // Short lived caches are dynamic holes in static prerenders, |
| 217 | + // so they shouldn't resolve in the static stage. |
| 218 | + assertLog(logs, 'after short-lived cache read - page', 'Server') |
| 219 | + assertLog(logs, 'after short-lived cache read - layout', 'Server') |
| 220 | + |
| 221 | + assertLog(logs, 'after uncached fetch - layout', 'Server') |
| 222 | + assertLog(logs, 'after uncached fetch - page', 'Server') |
| 223 | + } |
| 224 | + |
| 225 | + if (isInitialLoad) { |
| 226 | + await testInitialLoad(path, assertLogs) |
| 227 | + } else { |
| 228 | + await testNavigation(path, assertLogs) |
| 229 | + } |
| 230 | + }) |
108 | 231 | }) |
109 | 232 |
|
110 | | - it('cached data + short-lived cached data', async () => { |
111 | | - const path = '/short-lived-cache' |
112 | | - const browser = await next.browser(path) |
| 233 | + it('request APIs resolve in the correct phase', async () => { |
| 234 | + const path = '/apis/123' |
113 | 235 |
|
114 | | - const assertLogs = async () => { |
| 236 | + const assertLogs = async (browser: Playwright) => { |
115 | 237 | const logs = await browser.log() |
116 | | - assertLog(logs, 'after cache read - layout', 'Prerender') |
117 | 238 | assertLog(logs, 'after cache read - page', 'Prerender') |
118 | 239 |
|
119 | | - // Short lived caches are dynamic holes in static prerenders, |
120 | | - // so they shouldn't resolve in the static stage. |
121 | | - assertLog(logs, 'after short-lived cache read - page', 'Server') |
122 | | - assertLog(logs, 'after short-lived cache read - layout', 'Server') |
123 | | - |
124 | | - assertLog(logs, 'after uncached fetch - layout', 'Server') |
125 | | - assertLog(logs, 'after uncached fetch - page', 'Server') |
| 240 | + for (const apiName of [ |
| 241 | + 'cookies', |
| 242 | + 'headers', |
| 243 | + // TODO(restart-on-cache-miss): these two are currently broken/flaky, |
| 244 | + // because they're created outside of render and can resolve too early. |
| 245 | + // This will be fixed in a follow-up. |
| 246 | + // 'params', |
| 247 | + // 'searchParams', |
| 248 | + 'connection', |
| 249 | + ]) { |
| 250 | + assertLog(logs, `after ${apiName}`, 'Server') |
| 251 | + } |
126 | 252 | } |
127 | 253 |
|
128 | | - // Initial load. |
129 | | - await assertLogs() |
130 | | - |
131 | | - // After another load (with warm caches) the logs should be the same. |
132 | | - await browser.loadPage(next.url + path) // clears old logs |
133 | | - await assertLogs() |
134 | | - |
135 | | - // After a revalidation the subsequent warmup render must discard stale |
136 | | - // cache entries. |
137 | | - // This should not affect the environment labels. |
138 | | - await next.fetch(`/revalidate?path=${encodeURIComponent(path)}`) |
139 | | - |
140 | | - await browser.loadPage(next.url + path) // clears old logs |
141 | | - await assertLogs() |
142 | | - }) |
143 | | - }) |
144 | | - |
145 | | - it('runtime/dynamic APIs', async () => { |
146 | | - const path = '/apis/123' |
147 | | - const browser = await next.browser(path) |
148 | | - |
149 | | - const assertLogs = async () => { |
150 | | - const logs = await browser.log() |
151 | | - assertLog(logs, 'after cache read - page', 'Prerender') |
152 | | - |
153 | | - for (const apiName of [ |
154 | | - 'cookies', |
155 | | - 'headers', |
156 | | - // TODO(restart-on-cache-miss): these two are currently broken/flaky, |
157 | | - // because they're created outside of render and can resolve too early. |
158 | | - // This will be fixed in a follow-up. |
159 | | - // 'params', |
160 | | - // 'searchParams', |
161 | | - 'connection', |
162 | | - ]) { |
163 | | - assertLog(logs, `after ${apiName}`, 'Server') |
| 254 | + if (isInitialLoad) { |
| 255 | + await testInitialLoad(path, assertLogs) |
| 256 | + } else { |
| 257 | + await testNavigation(path, assertLogs) |
164 | 258 | } |
165 | | - } |
166 | | - |
167 | | - // Initial load. |
168 | | - await assertLogs() |
169 | | - |
170 | | - // After another load (with warm caches) the logs should be the same. |
171 | | - await browser.loadPage(next.url + path) // clears old logs |
172 | | - await assertLogs() |
173 | | - |
174 | | - // After a revalidation the subsequent warmup render must discard stale |
175 | | - // cache entries. |
176 | | - // This should not affect the environment labels. |
177 | | - await next.fetch(`/revalidate?path=${encodeURIComponent(path)}`) |
178 | | - |
179 | | - await browser.loadPage(next.url + path) // clears old logs |
180 | | - await assertLogs() |
| 259 | + }) |
181 | 260 | }) |
182 | 261 | }) |
0 commit comments