Repository: vercel/next.js
Pull request: [test] Deflake `app-dir-prevent-304-caching`

The test relied on the initial request hitting a stale page which triggers background revalidation. Turbopack builds fast enough that the page could still be fresh. Webpack always hit a stale page. We can't make all builds fast enough to guarantee a fresh hit. But we can wait a bit to ensure the request will hit a stale page.

This adds `next.waitForMinPrerenderAge(timeMS)` which ensures a subsequent request hits a prerender with at least `timeMS` age. Dev and deploy tests are assumed to always hit stale content. Either because the page wasn't prerendered (dev) or build was long enough in the past (deploy).

```
FAIL Turbopack test/production/app-dir-prevent-304-caching/index.test.ts (9.147 s)
  app-dir-prevent-304-caching
    ✕ should not cache 304 status (602 ms)

  ● app-dir-prevent-304-caching › should not cache 304 status

    expect(received).toBe(expected) // Object.is equality

    Expected: 200
    Received: 304

      36 |       }
      37 |     )
    > 38 |     expect(rHit.status).toBe(200)
         |                         ^
      39 |     await waitFor(500)
      40 |
      41 |     // Here happens the race condition

      at Object.toBe (production/app-dir-prevent-304-caching/index.test.ts:38:25)

```

Changed files:
filename: test/lib/next-modes/base.ts
patch: @@ -904,4 +904,9 @@ export class NextInstance {
       return this.cliOutput.slice(length)
     }
   }
+
+  public async waitForMinPrerenderAge(minAgeMS: number): Promise<void> {
+    // For tests we usually have a low revalidate time.
+    // We assume the prerender is old enough by default for those small revalidation times.
+  }
 }

filename: test/lib/next-modes/next-start.ts
patch: @@ -10,6 +10,8 @@ export class NextStartInstance extends NextInstance {
   private _buildId: string
   private _cliOutput: string = ''
 
+  private _prerenderFinishedTimeMS: number | null = null
+
   public get buildId() {
     return this._buildId
   }
@@ -80,6 +82,16 @@ export class NextStartInstance extends NextInstance {
               )
             else resolve()
           })
+          const prerenderedCallback = (msg: string) => {
+            const colorStrippedMsg = stripAnsi(msg)
+            // This stage happens after all prerenders have finished.
+            const prerenderFinishedPattern = /Finalizing page optimization/
+            if (prerenderFinishedPattern.test(colorStrippedMsg)) {
+              this._prerenderFinishedTimeMS = performance.now()
+              this.off('stdout', prerenderedCallback)
+            }
+          }
+          this.on('stdout', prerenderedCallback)
         } catch (err) {
           require('console').error(
             `Failed to run ${shellQuote(buildArgs)}`,
@@ -227,4 +239,26 @@ export class NextStartInstance extends NextInstance {
       })
     })
   }
+
+  public async waitForMinPrerenderAge(minAgeMS: number): Promise<void> {
+    if (this._prerenderFinishedTimeMS === null) {
+      throw new Error(
+        'Could not determine when prerender finished. ' +
+          `Cannot guarantee a minimum prerender age of ${minAgeMS}ms.`
+      )
+    }
+
+    const prerenderAge = performance.now() - this._prerenderFinishedTimeMS
+    const minWaitTime = minAgeMS - prerenderAge
+    if (minWaitTime > 0) {
+      console.log(
+        'Need to wait %dms to guarantee prerender age of %dms',
+        minWaitTime,
+        minAgeMS
+      )
+      await new Promise((resolve) => {
+        setTimeout(resolve, minWaitTime)
+      })
+    }
+  }
 }

filename: test/production/app-dir-prevent-304-caching/index.test.ts
patch: @@ -18,9 +18,10 @@ describe('app-dir-prevent-304-caching', () => {
 
   // https://github.com/vercel/next.js/issues/56580
   it('should not cache 304 status', async () => {
-    // Fresh call
-    const rFresh = await fetchViaHTTP(next.url, '/')
-    expect(rFresh.status).toBe(200)
+    // Ensure the first request hits a stale page triggering background revalidation.
+    await next.waitForMinPrerenderAge(1000 + 50)
+    const rStale = await fetchViaHTTP(next.url, '/')
+    expect(rStale.status).toBe(200)
 
     await waitFor(500)
 
@@ -31,7 +32,7 @@ describe('app-dir-prevent-304-caching', () => {
       {},
       {
         headers: {
-          'If-None-Match': rFresh.headers.get('etag'),
+          'If-None-Match': rStale.headers.get('etag'),
         },
       }
     )