Repository: vercel/next.js
Pull request: [test] Deflake root-optional-revalidate

Fixes https://github.com/vercel/next.js/actions/runs/18951850433/job/54118370618?pr=85571#step:34:217

I suspect that hitting the page right when revalidate expires is too fast. Now we expect that we called userspace revalidate right at 1s and then retry until the cache is filled since that's not based on a timer but performance.

Changed files:
filename: test/integration/root-optional-revalidate/pages/[[...slug]].js
patch: @@ -13,7 +13,10 @@ export async function getStaticPaths() {
   }
 }
 
-export async function getStaticProps({ params }) {
+export async function getStaticProps({ params, revalidateReason }) {
+  console.log(
+    `getStaticProps({ revalidateReason: ${JSON.stringify(revalidateReason)} })`
+  )
   return {
     props: {
       params,

filename: test/integration/root-optional-revalidate/test/index.test.js
patch: @@ -1,76 +0,0 @@
-/* eslint-env jest */
-
-import { join } from 'path'
-import cheerio from 'cheerio'
-import {
-  killApp,
-  findPort,
-  nextBuild,
-  nextStart,
-  renderViaHTTP,
-  waitFor,
-} from 'next-test-utils'
-
-const appDir = join(__dirname, '../')
-let app
-let appPort
-
-const getProps = async (path) => {
-  const html = await renderViaHTTP(appPort, path)
-  const $ = cheerio.load(html)
-  return JSON.parse($('#props').text())
-}
-
-const runTests = () => {
-  it('should render / correctly', async () => {
-    const props = await getProps('/')
-    expect(props.params).toEqual({})
-
-    await waitFor(1000)
-    await getProps('/')
-
-    const newProps = await getProps('/')
-    expect(newProps.params).toEqual({})
-    expect(props.random).not.toBe(newProps.random)
-  })
-
-  it('should render /a correctly', async () => {
-    const props = await getProps('/a')
-    expect(props.params).toEqual({ slug: ['a'] })
-
-    await waitFor(1000)
-    await getProps('/a')
-
-    const newProps = await getProps('/a')
-    expect(newProps.params).toEqual({ slug: ['a'] })
-    expect(props.random).not.toBe(newProps.random)
-  })
-
-  it('should render /hello/world correctly', async () => {
-    const props = await getProps('/hello/world')
-    expect(props.params).toEqual({ slug: ['hello', 'world'] })
-
-    await waitFor(1000)
-    await getProps('/hello/world')
-
-    const newProps = await getProps('/hello/world')
-    expect(newProps.params).toEqual({ slug: ['hello', 'world'] })
-    expect(props.random).not.toBe(newProps.random)
-  })
-}
-
-describe('Root Optional Catch-all Revalidate', () => {
-  ;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
-    'production mode',
-    () => {
-      beforeAll(async () => {
-        await nextBuild(appDir)
-        appPort = await findPort()
-        app = await nextStart(appDir, appPort)
-      })
-      afterAll(() => killApp(app))
-
-      runTests()
-    }
-  )
-})

filename: test/integration/root-optional-revalidate/test/index.test.ts
patch: @@ -0,0 +1,94 @@
+import type { ChildProcess } from 'child_process'
+import { join } from 'path'
+import cheerio from 'cheerio'
+import {
+  killApp,
+  findPort,
+  nextBuild,
+  nextStart,
+  renderViaHTTP,
+  waitFor,
+  retry,
+} from 'next-test-utils'
+
+const appDir = join(__dirname, '../')
+let app: ChildProcess | undefined
+let appPort: number | undefined
+
+const getProps = async (path) => {
+  const html = await renderViaHTTP(appPort, path)
+  const $ = cheerio.load(html)
+  return JSON.parse($('#props').text())
+}
+
+const runTests = () => {
+  it('should render / correctly', async () => {
+    const props = await getProps('/')
+    expect(props.params).toEqual({})
+
+    let cliOutput = ''
+    app.stdout.on('data', (chunk) => (cliOutput += chunk.toString()))
+    await waitFor(1000)
+    await getProps('/')
+    expect(cliOutput).toEqual('getStaticProps({ revalidateReason: "stale" })\n')
+
+    // Rendering takes time before the new cache entry is filled
+    await retry(async () => {
+      const newProps = await getProps('/')
+      expect(newProps.params).toEqual({})
+      expect(props.random).not.toBe(newProps.random)
+    })
+  })
+
+  it('should render /a correctly', async () => {
+    const props = await getProps('/a')
+    expect(props.params).toEqual({ slug: ['a'] })
+
+    let cliOutput = ''
+    app.stdout.on('data', (chunk) => (cliOutput += chunk.toString()))
+    await waitFor(1000)
+    await getProps('/a')
+    expect(cliOutput).toEqual('getStaticProps({ revalidateReason: "stale" })\n')
+
+    // Rendering takes time before the new cache entry is filled
+    await retry(async () => {
+      const newProps = await getProps('/a')
+      expect(newProps.params).toEqual({ slug: ['a'] })
+      expect(props.random).not.toBe(newProps.random)
+    })
+  })
+
+  it('should render /hello/world correctly', async () => {
+    const props = await getProps('/hello/world')
+    expect(props.params).toEqual({ slug: ['hello', 'world'] })
+
+    let cliOutput = ''
+    app.stdout.on('data', (chunk) => (cliOutput += chunk.toString()))
+    await waitFor(1000)
+    await getProps('/hello/world')
+    expect(cliOutput).toEqual('getStaticProps({ revalidateReason: "stale" })\n')
+
+    // Rendering takes time before the new cache entry is filled
+    await retry(async () => {
+      const newProps = await getProps('/hello/world')
+      expect(newProps.params).toEqual({ slug: ['hello', 'world'] })
+      expect(props.random).not.toBe(newProps.random)
+    })
+  })
+}
+
+describe('Root Optional Catch-all Revalidate', () => {
+  ;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
+    'production mode',
+    () => {
+      beforeAll(async () => {
+        await nextBuild(appDir)
+        appPort = await findPort()
+        app = await nextStart(appDir, appPort)
+      })
+      afterAll(() => killApp(app))
+
+      runTests()
+    }
+  )
+})