Repository: vercel/next.js
Pull request: [test] Deflake `instant-navs-devtools`

Mostly using `elementByCss` instead of `browser.eval` to use built-in retry logic as well as ensuring we assert on accessible content.

Changed files:
filename: test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts
patch: @@ -1,114 +1,72 @@
 import { nextTestSetup } from 'e2e-utils'
-import {
-  retry,
-  waitForDevToolsIndicator,
-  toggleDevToolsIndicatorPopover,
-} from 'next-test-utils'
+import { retry, toggleDevToolsIndicatorPopover } from 'next-test-utils'
+import { Playwright } from 'next-webdriver'
 
 describe('instant-nav-panel', () => {
   const { next } = nextTestSetup({
     files: __dirname,
   })
 
-  async function clearInstantModeCookie(browser: any) {
-    await browser.eval(() => {
-      document.cookie = 'next-instant-navigation-testing=; path=/; max-age=0'
-    })
-  }
-
-  async function clickInstantNavMenuItem(browser: any) {
-    await browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      portal?.shadowRoot?.querySelector('[data-instant-nav]')?.click()
-    })
+  async function waitForPanelRouterTransition() {
+    // Run all the necessary CSS transitions
+    // and click-outside event handler adjustment due to cascading update.
+    // TODO: Consider disabling transitions entirely in Next.js tests.
+    await new Promise((resolve) =>
+      setTimeout(
+        resolve,
+        // MENU_DURATION_MS
+        200
+      )
+    )
   }
 
-  async function clickStartClientNav(browser: any) {
+  async function clearInstantModeCookie(browser: Playwright) {
     await browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      portal?.shadowRoot?.querySelector('[data-instant-nav-client]')?.click()
+      document.cookie = 'next-instant-navigation-testing=; path=/; max-age=0'
     })
   }
 
-  async function getBadgeStatus(browser: any): Promise<string> {
-    return browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      return (
-        portal?.shadowRoot
-          ?.querySelector('[data-next-badge]')
-          ?.getAttribute('data-status') || ''
-      )
-    })
+  async function clickInstantNavMenuItem(browser: Playwright) {
+    await browser.elementByCss('[data-instant-nav]').click()
   }
 
-  async function getPanelText(browser: any): Promise<string> {
-    return browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      const panel = portal?.shadowRoot?.querySelector('.instant-nav-panel')
-      return panel?.innerText || ''
-    })
+  async function clickStartClientNav(browser: Playwright) {
+    await browser.elementByCssInstant('[data-instant-nav-client]').click()
   }
 
-  async function hasPanelOpen(browser: any): Promise<boolean> {
-    return browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      return !!portal?.shadowRoot?.querySelector('.instant-nav-panel')
-    })
+  async function getInstantNavPanelText(browser: Playwright): Promise<string> {
+    return browser.elementByCssInstant('.instant-nav-panel').text()
   }
 
-  async function closePanelViaHeader(browser: any) {
-    await browser.eval(() => {
-      const portal = [].slice
-        .call(document.querySelectorAll('nextjs-portal'))
-        .find((p: any) =>
-          p.shadowRoot.querySelector('[data-nextjs-toast]')
-        ) as any
-      portal?.shadowRoot?.querySelector('#_next-devtools-panel-close')?.click()
-    })
+  async function closePanelViaHeader(browser: Playwright) {
+    return browser.elementByCss('#_next-devtools-panel-close').click()
   }
 
-  async function openInstantNavPanel(browser: any) {
-    await waitForDevToolsIndicator(browser)
+  async function openInstantNavPanel(browser: Playwright) {
     await toggleDevToolsIndicatorPopover(browser)
+    await waitForPanelRouterTransition()
     await clickInstantNavMenuItem(browser)
+
+    await retry(
+      async () => {
+        await browser.elementByCssInstant('.instant-nav-panel')
+      },
+      5_000,
+      500
+    )
+    await waitForPanelRouterTransition()
   }
 
   it('should open panel in waiting state without setting cookie', async () => {
     const browser = await next.browser('/')
     await clearInstantModeCookie(browser)
     await browser.waitForElementByCss('[data-testid="home-title"]')
 
-    // Wait for initial compilation to settle
-    await retry(async () => {
-      const status = await getBadgeStatus(browser)
-      expect(status).toBe('none')
-    })
-
     await openInstantNavPanel(browser)
 
     // Panel should show waiting state with Page load and Client navigation sections
     await retry(async () => {
-      const text = await getPanelText(browser)
+      const text = await getInstantNavPanelText(browser)
       expect(text).toContain('Page load')
       expect(text).toContain('Client navigation')
     })
@@ -126,19 +84,8 @@ describe('instant-nav-panel', () => {
     await clearInstantModeCookie(browser)
     await browser.waitForElementByCss('[data-testid="home-title"]')
 
-    // Wait for initial compilation to settle (tsconfig creation triggers Fast Refresh)
-    await retry(async () => {
-      const status = await getBadgeStatus(browser)
-      expect(status).toBe('none')
-    })
-
     await openInstantNavPanel(browser)
 
-    // Wait for panel to be open
-    await retry(async () => {
-      expect(await hasPanelOpen(browser)).toBe(true)
-    })
-
     // Click Start to enter client-nav-waiting state
     await clickStartClientNav(browser)
 
@@ -150,7 +97,7 @@ describe('instant-nav-panel', () => {
 
     // Panel should show client-nav-waiting state
     await retry(async () => {
-      const text = await getPanelText(browser)
+      const text = await getInstantNavPanelText(browser)
       expect(text).toContain('Client navigation')
       expect(text).toContain('Click any link')
     })
@@ -162,7 +109,7 @@ describe('instant-nav-panel', () => {
 
     // Panel should transition to client-nav state
     await retry(async () => {
-      const text = await getPanelText(browser)
+      const text = await getInstantNavPanelText(browser)
       expect(text).toContain('Client navigation')
       expect(text).toContain('prefetched UI')
       expect(text).toContain('Continue rendering')
@@ -179,11 +126,6 @@ describe('instant-nav-panel', () => {
 
     await openInstantNavPanel(browser)
 
-    // Wait for panel to be open
-    await retry(async () => {
-      expect(await hasPanelOpen(browser)).toBe(true)
-    })
-
     // Click Start to activate the navigation lock
     await clickStartClientNav(browser)
 

filename: test/development/app-dir/instant-navs-devtools/tsconfig.json
patch: @@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": false,
+    "noEmit": true,
+    "incremental": true,
+    "module": "esnext",
+    "esModuleInterop": true,
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "react-jsx",
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ]
+  },
+  "include": ["next-env.d.ts", "**/*.mts", "**/*.ts", "**/*.tsx"],
+  "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}