Repository: vercel/next.js
Pull request: [test] Improve React performance track tests

Fixes some potential flakiness due to hydration firing before all boundaries are revealed.

Also adds a live view of the Server Requests so that you don't need to start profiling to see the requests.

Changed files:
filename: test/development/app-dir/react-performance-track/app/ReactServerRequests.tsx
patch: @@ -0,0 +1,42 @@
+'use client'
+
+import { useSyncExternalStore } from 'react'
+
+const reactServerRequestsServerSnapshot: never[] = []
+const ServerRequestsStore = {
+  subscribe:
+    typeof window === 'undefined'
+      ? () => () => {}
+      : window.reactServerRequests.subscribe,
+  getSnapshot:
+    typeof window === 'undefined'
+      ? () => []
+      : window.reactServerRequests.getSnapshot,
+  getServerSnapshot: () => reactServerRequestsServerSnapshot,
+}
+
+export function ReactServerRequests() {
+  const reactServerRequests = useSyncExternalStore(
+    ServerRequestsStore.subscribe,
+    ServerRequestsStore.getSnapshot,
+    ServerRequestsStore.getServerSnapshot
+  )
+
+  return (
+    <>
+      <p>{reactServerRequests.length} Server Request entries</p>
+      <ol>
+        {reactServerRequests.map((request, index) => (
+          <li key={index}>
+            <details>
+              <summary>
+                <strong>{request.name}</strong>
+              </summary>
+              <pre>{JSON.stringify(request.properties, null, 2)}</pre>
+            </details>
+          </li>
+        ))}
+      </ol>
+    </>
+  )
+}

filename: test/development/app-dir/react-performance-track/app/layout.tsx
patch: @@ -1,9 +1,29 @@
 import { ReactNode, Suspense } from 'react'
+import { ReactServerRequests } from './ReactServerRequests'
+import Link from 'next/link'
+
 export default function Root({ children }: { children: ReactNode }) {
   return (
     <html>
-      <body>
-        <Suspense>{children}</Suspense>
+      <body style={{ display: 'flex', flexDirection: 'row', gap: '1rem' }}>
+        <ul>
+          <li>
+            <Link href="/">Home</Link>
+          </li>
+          <li>
+            <Link href="/fetch">fetch</Link>
+          </li>
+          <li>
+            <Link href="/set-timeout">setTimeout</Link>
+          </li>
+        </ul>
+        <main>
+          <ReactServerRequests />
+          <Suspense fallback="Loading Server Requests">
+            <div data-react-server-requests-done />
+            {children}
+          </Suspense>
+        </main>
       </body>
     </html>
   )

filename: test/development/app-dir/react-performance-track/app/page.tsx
patch: @@ -1,5 +1,3 @@
-const error = new Error('test')
 export default function Page() {
-  console.error(error)
-  return <p>hello world</p>
+  return <p>Hello, World!</p>
 }

filename: test/development/app-dir/react-performance-track/instrumentation-client.ts
patch: @@ -1,33 +1,60 @@
 export {}
 
+type ReactServerRequests = Array<{
+  name: string
+  properties: any
+}>
+
 declare global {
   interface Window {
-    reactServerRequests: Array<{
-      name: string
-      properties: any
-    }>
+    reactServerRequests: {
+      getSnapshot(): ReactServerRequests
+      subscribe(callback: () => void): () => void
+    }
   }
 
   interface PerformanceEntry {
     detail?: any
   }
 }
 
-window.reactServerRequests = []
+let reactServerRequests: ReactServerRequests = []
+const listeners = new Set<() => void>()
+
+// The store implementation is just a local debugging aid.
+// Assertions should happen on `getSnapshot` not on the UI.
+window.reactServerRequests = {
+  getSnapshot: () => {
+    return reactServerRequests
+  },
+  subscribe: (callback) => {
+    listeners.add(callback)
+    return () => {
+      listeners.delete(callback)
+    }
+  },
+}
 
 // We're trying to mock how the Chrome DevTools performance panel will display
 // React performance data. React might decide to use console.timeStamp instead
 // or any other method that will be picked up by the performance panel so this
 // logic may have to be adjusted when updating React. A change here, doesn't
 // mean it's a breaking change in React nor Next.js.
 new PerformanceObserver((entries) => {
+  const newRequests: ReactServerRequests = []
   for (const entry of entries.getEntries()) {
     if (entry.detail?.devtools?.track === 'Server Requests ⚛') {
-      window.reactServerRequests.push({
+      newRequests.push({
         name: entry.name,
         properties: entry.detail.devtools.properties,
       })
     }
   }
-  console.log(window.reactServerRequests)
+
+  if (newRequests.length > 0) {
+    reactServerRequests = reactServerRequests.concat(newRequests)
+    for (const listener of listeners) {
+      listener()
+    }
+  }
 }).observe({ entryTypes: ['measure'] })

filename: test/development/app-dir/react-performance-track/react-performance-track.test.ts
patch: @@ -7,8 +7,9 @@ describe('react-performance-track', () => {
 
   it('should show setTimeout', async () => {
     const browser = await next.browser('/set-timeout')
+    await browser.elementByCss('[data-react-server-requests-done]')
 
-    const track = await browser.eval('window.reactServerRequests')
+    const track = await browser.eval('window.reactServerRequests.getSnapshot()')
     expect(track).toEqual([
       { name: 'setTimeout', properties: [] },
       { name: 'setTimeout', properties: [] },
@@ -17,9 +18,10 @@ describe('react-performance-track', () => {
 
   it('should show fetch', async () => {
     const browser = await next.browser('/fetch')
+    await browser.elementByCss('[data-react-server-requests-done]')
 
-    const track = await browser.eval('window.reactServerRequests')
-    // FIXME: Should show await fetch and await response.json()
+    const track = await browser.eval('window.reactServerRequests.getSnapshot()')
+    // FIXME: Should show await fetch
     expect(track).toEqual([])
   })
 })