Repository: vercel/next.js
Pull request: Show labeled steps for `instant()` test helper in the Playwright UI

Stacked on #90613

When `@playwright/test` is available, the `instant()` helper now wraps its cookie-based lock acquire and release in `test.step()` calls. This surfaces "Acquire Instant Lock" and "Release Instant Lock" as distinct, labeled entries in the Playwright UI action list instead of anonymous `browserContext.addCookies` calls.

`@playwright/test` is added as an optional peer dependency of `@next/playwright`. When it is not installed, the helper behaves exactly as before.

Before:

<img width="251" alt="before" src="https://github.com/user-attachments/assets/6c68b1d2-012d-4ff9-aa72-9f931dfe55ed" />

After:

<img width="251" alt="after" src="https://github.com/user-attachments/assets/0156245d-b52f-4542-b22c-7d538aea5810" />

Changed files:
filename: packages/next-playwright/package.json
patch: @@ -17,6 +17,14 @@
     "dev": "tsc -d -w -p tsconfig.json",
     "typescript": "tsec --noEmit -p tsconfig.json"
   },
+  "peerDependencies": {
+    "@playwright/test": ">=1.0.0"
+  },
+  "peerDependenciesMeta": {
+    "@playwright/test": {
+      "optional": true
+    }
+  },
   "devDependencies": {
     "typescript": "5.9.2"
   }

filename: packages/next-playwright/src/index.ts
patch: @@ -1,3 +1,5 @@
+import { step } from './step'
+
 /**
  * Minimal interfaces for Playwright's Page and BrowserContext. We use
  * structural types rather than importing from a specific Playwright package
@@ -41,6 +43,9 @@ const INSTANT_COOKIE = 'next-instant-navigation-testing'
  *     await page.goto(url)
  *     // ...
  *   }, { baseURL: 'http://localhost:3000' })
+ *
+ * When `@playwright/test` is installed, acquire/release actions appear
+ * as labeled steps in the Playwright UI.
  */
 export async function instant<T>(
   page: PlaywrightPage,
@@ -52,19 +57,23 @@ export async function instant<T>(
   // The cookie triggers the CookieStore change event in
   // navigation-testing-lock.ts, which acquires the in-memory navigation lock.
   const { hostname } = new URL(resolveURL(page, options))
-  await page
-    .context()
-    .addCookies([
-      { name: INSTANT_COOKIE, value: '1', domain: hostname, path: '/' },
-    ])
+  await step('Acquire Instant Lock', () =>
+    page
+      .context()
+      .addCookies([
+        { name: INSTANT_COOKIE, value: '1', domain: hostname, path: '/' },
+      ])
+  )
   try {
     return await fn()
   } finally {
     // Release the lock by clearing the cookie. For SPA navigations, this
     // triggers the CookieStore change event which resolves the in-memory
     // lock. For MPA navigations (reload, plain anchor), the listener in
     // app-bootstrap.ts triggers a page reload to fetch dynamic data.
-    await page.context().clearCookies({ name: INSTANT_COOKIE })
+    await step('Release Instant Lock', () =>
+      page.context().clearCookies({ name: INSTANT_COOKIE })
+    )
   }
 }
 

filename: packages/next-playwright/src/step.ts
patch: @@ -0,0 +1,32 @@
+export type Step = <T>(title: string, body: () => Promise<T>) => Promise<T>
+
+/**
+ * When `@playwright/test` is installed and we're running inside the Playwright
+ * test runner, wraps the body in a `test.step()` so it appears as a labeled
+ * step in the Playwright UI. Otherwise just executes the body directly.
+ */
+let step: Step = (_title, body) => body()
+try {
+  const pw = require('@playwright/test') as typeof import('@playwright/test')
+  if (typeof pw.test?.step === 'function') {
+    const playwrightStep = pw.test.step
+    step = async (title, body) => {
+      try {
+        return await playwrightStep(title, body)
+      } catch (e) {
+        // If test.step fails because we're not inside the Playwright test
+        // runner (e.g., running under Jest), fall back to executing the body
+        // directly without step labels.
+        if (
+          e instanceof Error &&
+          e.message.includes('can only be called from a test')
+        ) {
+          return body()
+        }
+        throw e
+      }
+    }
+  }
+} catch {}
+
+export { step }

filename: pnpm-lock.yaml
patch: @@ -1866,6 +1866,10 @@ importers:
         version: 0.7.3
 
   packages/next-playwright:
+    dependencies:
+      '@playwright/test':
+        specifier: '>=1.0.0'
+        version: 1.51.1
     devDependencies:
       typescript:
         specifier: 5.9.2