Repository: vercel/next.js
Pull request: [Cache Components] Schedule work on timeouts

Previously we used the immediate queue to schedule the consecutive tasks that would prerender and abort and page prerender. This works fine but since React does not consider immediates as IO for async work tracking it means we can't use it for scheduling more advanced cases like static->runtime->dynamic where the IO that unblocks in runtime phase can be picked up.

While React could change to include immediates as IO the argument here is that it isn't really IO since it is immediate work while timeouts generally have to schedule something to fire later with timeout zero being a sort of special case where later === now.

So instead we are going to move to scheduling our consecutive tasks using timeout as well that way we can align the Next.js and React heuristic for identifying IO and make certain kinds of debugging easier to implement.

Changed files:
filename: packages/next/src/server/app-render/app-render-prerender-utils.ts
patch: @@ -2,7 +2,7 @@ import { InvariantError } from '../../shared/lib/invariant-error'
 
 /**
  * This is a utility function to make scheduling sequential tasks that run back to back easier.
- * We schedule on the same queue (setImmediate) at the same time to ensure no other events can sneak in between.
+ * We schedule on the same queue (setTimeout) at the same time to ensure no other events can sneak in between.
  */
 export function prerenderAndAbortInSequentialTasks<R>(
   prerender: () => Promise<R>,
@@ -15,18 +15,18 @@ export function prerenderAndAbortInSequentialTasks<R>(
   } else {
     return new Promise((resolve, reject) => {
       let pendingResult: Promise<R>
-      setImmediate(() => {
+      setTimeout(() => {
         try {
           pendingResult = prerender()
           pendingResult.catch(() => {})
         } catch (err) {
           reject(err)
         }
-      })
-      setImmediate(() => {
+      }, 0)
+      setTimeout(() => {
         abort()
         resolve(pendingResult)
-      })
+      }, 0)
     })
   }
 }
@@ -47,21 +47,21 @@ export function prerenderAndAbortInSequentialTasksWithStages<R>(
   } else {
     return new Promise((resolve, reject) => {
       let pendingResult: Promise<R>
-      setImmediate(() => {
+      setTimeout(() => {
         try {
           pendingResult = prerender()
           pendingResult.catch(() => {})
         } catch (err) {
           reject(err)
         }
-      })
-      setImmediate(() => {
+      }, 0)
+      setTimeout(() => {
         advanceStage()
-      })
-      setImmediate(() => {
+      }, 0)
+      setTimeout(() => {
         abort()
         resolve(pendingResult)
-      })
+      }, 0)
     })
   }
 }

filename: packages/next/src/server/app-render/app-render-render-utils.ts
patch: @@ -2,7 +2,7 @@ import { InvariantError } from '../../shared/lib/invariant-error'
 
 /**
  * This is a utility function to make scheduling sequential tasks that run back to back easier.
- * We schedule on the same queue (setImmediate) at the same time to ensure no other events can sneak in between.
+ * We schedule on the same queue (setTimeout) at the same time to ensure no other events can sneak in between.
  */
 export function scheduleInSequentialTasks<R>(
   render: () => R | Promise<R>,
@@ -15,17 +15,17 @@ export function scheduleInSequentialTasks<R>(
   } else {
     return new Promise((resolve, reject) => {
       let pendingResult: R | Promise<R>
-      setImmediate(() => {
+      setTimeout(() => {
         try {
           pendingResult = render()
         } catch (err) {
           reject(err)
         }
-      })
-      setImmediate(() => {
+      }, 0)
+      setTimeout(() => {
         followup()
         resolve(pendingResult)
-      })
+      }, 0)
     })
   }
 }

filename: packages/next/src/server/lib/patch-fetch.ts
patch: @@ -27,7 +27,6 @@ import {
   type ServerComponentsHmrCache,
   type SetIncrementalFetchCacheContext,
 } from '../response-cache'
-import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
 import { cloneResponse } from './clone-response'
 import type { IncrementalCache } from './incremental-cache'
 
@@ -911,7 +910,7 @@ export function createPatchedFetcher(
                   // make sure we still exclude them from prerenders if
                   // cacheComponents is on so we introduce an artificial task boundary
                   // here.
-                  await waitAtLeastOneReactRenderTask()
+                  await getTimeoutBoundary()
                   break
                 case 'prerender-ppr':
                 case 'prerender-legacy':
@@ -1191,3 +1190,16 @@ export function patchFetch(options: PatchableModule) {
   // Set the global fetch to the patched fetch.
   globalThis.fetch = createPatchedFetcher(original, options)
 }
+
+let currentTimeoutBoundary: null | Promise<void> = null
+function getTimeoutBoundary() {
+  if (!currentTimeoutBoundary) {
+    currentTimeoutBoundary = new Promise((r) => {
+      setTimeout(() => {
+        currentTimeoutBoundary = null
+        r()
+      }, 0)
+    })
+  }
+  return currentTimeoutBoundary
+}

filename: test/development/app-dir/cache-components-dev-fallback-validation/cache-components-dev-fallback-validation.test.ts
patch: @@ -55,11 +55,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -70,11 +70,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -88,11 +88,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -103,11 +103,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -121,11 +121,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -136,11 +136,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/partial/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31) @ Page
-       > 1 | export default async function Page(props: {
-           |                               ^",
+         "source": "app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26) @ Page
+       > 6 |       Top: {(await props.params).top}, Bottom: {(await props.params).bottom}
+           |                          ^",
          "stack": [
-           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (1:31)",
+           "Page app/partial/[top]/unwrapped/[bottom]/page.tsx (6:26)",
            "LogSafely <anonymous>",
          ],
        }
@@ -158,11 +158,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -173,11 +173,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -191,11 +191,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -206,11 +206,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -224,11 +224,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -239,11 +239,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/wrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/wrapped/layout.tsx (3:31) @ Layout
-       > 3 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/wrapped/layout.tsx (10:3) @ Layout
+       > 10 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/wrapped/layout.tsx (3:31)",
+           "Layout app/none/[top]/wrapped/layout.tsx (10:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -257,11 +257,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -272,11 +272,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -290,11 +290,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -305,11 +305,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -323,11 +323,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }
@@ -338,11 +338,11 @@ describe('Cache Components Fallback Validation', () => {
          "description": "Route "/none/[top]/unwrapped/[bottom]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
          "environmentLabel": "Server",
          "label": "Console Error",
-         "source": "app/none/[top]/unwrapped/layout.tsx (1:31) @ Layout
-       > 1 | export default async function Layout({
-           |                               ^",
+         "source": "app/none/[top]/unwrapped/layout.tsx (8:3) @ Layout
+       >  8 |   await params
+            |   ^",
          "stack": [
-           "Layout app/none/[top]/unwrapped/layout.tsx (1:31)",
+           "Layout app/none/[top]/unwrapped/layout.tsx (8:3)",
            "LogSafely <anonymous>",
          ],
        }

filename: test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
patch: @@ -159,11 +159,11 @@ describe('Cache Components Errors', () => {
              "description": "Route "/dynamic-metadata-error-route": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
              "environmentLabel": "Server",
              "label": "Console Error",
-             "source": "app/dynamic-metadata-error-route/page.tsx (21:9) @ Dynamic
-           > 21 |   await new Promise((r) => setTimeout(r))
-                |         ^",
+             "source": "app/dynamic-metadata-error-route/page.tsx (20:16) @ Dynamic
+           > 20 | async function Dynamic() {
+                |                ^",
              "stack": [
-               "Dynamic app/dynamic-metadata-error-route/page.tsx (21:9)",
+               "Dynamic app/dynamic-metadata-error-route/page.tsx (20:16)",
                "Page app/dynamic-metadata-error-route/page.tsx (15:7)",
                "LogSafely <anonymous>",
              ],
@@ -3081,35 +3081,13 @@ describe('Cache Components Errors', () => {
             )
 
             if (isTurbopack) {
-              await expect(browser).toDisplayCollapsedRedbox(`
-               {
-                 "description": "Route "/use-cache-private-without-suspense": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
-                 "environmentLabel": "Server",
-                 "label": "Console Error",
-                 "source": "app/use-cache-private-without-suspense/page.tsx (10:7) @ Page
-               > 10 |       <Private />
-                    |       ^",
-                 "stack": [
-                   "Page app/use-cache-private-without-suspense/page.tsx (10:7)",
-                   "LogSafely <anonymous>",
-                 ],
-               }
-              `)
+              await expect(browser).toDisplayCollapsedRedbox(
+                `"Redbox did not open."`
+              )
             } else {
-              await expect(browser).toDisplayCollapsedRedbox(`
-               {
-                 "description": "Route "/use-cache-private-without-suspense": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
-                 "environmentLabel": "Server",
-                 "label": "Console Error",
-                 "source": "app/use-cache-private-without-suspense/page.tsx (10:7) @ Page
-               > 10 |       <Private />
-                    |       ^",
-                 "stack": [
-                   "Page app/use-cache-private-without-suspense/page.tsx (10:7)",
-                   "LogSafely <anonymous>",
-                 ],
-               }
-              `)
+              await expect(browser).toDisplayCollapsedRedbox(
+                `"Redbox did not open."`
+              )
             }
           })
         } else {