Home/Testing & QA/next-browser
N

next-browser

by @vercel-labsv
4.4(43)

Automate installation and updates of the next-browser tool, integrating with Playwright to set up browser testing environments for Next.js applications.

nextjsbrowser-automationplaywrighttestingfrontendGitHub
Installation
npx skills add vercel-labs/next-browser --skill next-browser
compare_arrows

Before / After Comparison

1
Before

Manually installing Playwright and browser drivers, version mismatches lead to test failures, and repeated configuration is required for every upgrade.

After

Automatically detects and installs the latest version of next-browser, one-click configuration of the test environment, and version matching issues are automatically resolved.

SKILL.md

next-browser

next-browser

If next-browser is not already on PATH, install @vercel/next-browser globally with the user's package manager, then playwright install chromium.

If next-browser is already installed, it may be outdated. Run next-browser --version and compare against the latest on npm (npm view @vercel/next-browser version). If the installed version is behind, upgrade it (npm install -g @vercel/next-browser@latest or the equivalent for the user's package manager) before proceeding.

Next.js docs awareness

If the project's Next.js version is v16.2.0-canary.37 or later, bundled docs live at node_modules/next/dist/docs/. Before doing PPR work, Cache Components work, or any non-trivial Next.js task, read the relevant doc there — your training data may be outdated. The bundled docs are the source of truth.

See https://nextjs.org/docs/app/guides/ai-agents for background.

When this skill loads

Your first message introduces the tool and asks setup questions. Don't say "ready, what would you like to do?" and don't run speculative commands or auto-discover (port scans, project, config reads).

If the user already provided a URL, cookies, and task in their message, skip the questions — go straight to open and start working. Only ask what's missing.

Otherwise say something like:

This opens a headed browser against your Next.js dev server so I can read the React component tree, see the PPR shell, and check errors the way you would in DevTools. To start:

  • What's your dev server URL? (And is it running?)

  • Are the pages you're debugging behind a login? If so I'll need your session cookies — easiest is to copy them from your browser's DevTools → Application → Cookies into a JSON file like [{"name":"session","value":"..."}]. If the pages are public, skip this.

Wait for answers. Then open <url> [--cookies-json <file>]. Every other command errors without an open session.

Commands

open <url> [--cookies-json <file>]

Launch browser, navigate to URL. With --cookies-json, sets auth cookies before navigating (domain derived from URL hostname).

$ next-browser open http://localhost:3024/vercel --cookies-json cookies.json
opened → http://localhost:3024/vercel (11 cookies for localhost)

Cookie file format: [{"name":"authorization","value":"Bearer ..."}, ...]

Only name and value are required per cookie — omit domain, path, expires, etc. To create the file, use Bash (echo '[...]' > /tmp/cookies.json) since the Write tool requires a prior Read.

close

Close browser and kill daemon.

goto <url>

Navigate to a URL with a fresh server render. The browser loads a new document — equivalent to typing a URL in the address bar.

$ next-browser goto http://localhost:3024/vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments

push [path]

Client-side navigation — the page transitions without a full reload, the way a user clicks a link in the app. Without a path, shows an interactive picker of all links on the current page.

$ next-browser push /vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments

If push fails silently (URL unchanged), the route wasn't prefetched.

back

Go back one page in browser history.

reload

Reload the current page from the server.

ssr-goto <url>

See exactly what the server sent before any client-side JavaScript runs. Useful for verifying SSR content, checking what search engines and social crawlers see, debugging hydration mismatches, and confirming that data appears in the initial HTML rather than being fetched client-side.

The page renders without hydration — no React, no client-side routing, no fetch calls. What you see is the raw server output plus CSS.

$ next-browser ssr-goto http://localhost:3000/dashboard
→ http://localhost:3000/dashboard (external scripts blocked)

Use goto or reload afterward to restore normal behavior.

perf [url]

Profile a full page load — reloads the current page (or navigates to a URL) and collects Core Web Vitals and React hydration timing in one pass.

$ next-browser perf http://localhost:3000/dashboard
# Page Load Profile — http://localhost:3000/dashboard

## Core Web Vitals
  TTFB                   42ms
  LCP               1205.3ms (img: /_next/image?url=...)
  CLS                    0.03

## React Hydration — 65.5ms (466.2ms → 531.7ms)
  Hydrated                         65.5ms  (466.2 → 531.7)
  Commit                            2.0ms  (531.7 → 533.7)
  Waiting for Paint                 3.0ms  (533.7 → 536.7)
  Remaining Effects                 4.1ms  (536.7 → 540.8)

## Hydrated components (42 total, sorted by duration)
  DeploymentsProvider                       8.3ms
  NavigationProvider                        5.1ms
  ...

TTFB — server response time (Navigation Timing API). LCP — when the largest visible element painted, plus what it was. CLS — cumulative layout shift score (lower is better). Hydration — React reconciler phases and per-component cost (requires React profiling build / next dev; production strips console.timeStamp).

Without a URL, reloads the current page. With a URL, navigates there first.

restart-server

Restart the Next.js dev server and clear its caches. Forces a clean recompile from scratch.

Last resort. HMR picks up code changes on its own — reach for this only when you have evidence the dev server is wedged (stale output after edits, builds that never finish, errors that don't clear).

Often exits with net::ERR_ABORTED — this is expected (the page detaches during restart). Follow up with goto <url> to re-navigate after the server is back. Don't treat this error as a failure.

ppr lock

Prerequisite: PPR requires cacheComponents to be enabled in next.config. Without it the shell won't have pre-rendered content to show.

Freeze dynamic content so you can inspect the static shell — the part of the page that's instantly available before any data loads. After locking:

  • goto — shows the server-rendered shell with holes where dynamic content would appear.

  • push — shows what the client already has from prefetching. Requires the current page to already be hydrated (prefetch is client-side), so lock after you've landed on the origin, not before.

$ next-browser ppr lock
locked

ppr unlock

Resume dynamic content and print a shell analysis — which Suspense boundaries were holes in the shell, what blocked them, and which were static. The output can be very large (hundreds of boundaries). Pipe through | head -20 if you only need the summary and dynamic holes.

$ next-browser ppr unlock
unlocked

# PPR Shell Analysis
# 131 boundaries: 3 dynamic holes, 128 static

## Summary
- Top actionable hole: TrackedSuspense — usePathname (client-hook)
- Suggested next step: This route segment is suspending on client hooks. Check loading.tsx first...
- Most common root cause: usePathname (client-hook) affecting 1 boundary

## Quick Reference
| Boundary                   | Type              | Fallback source | Primary blocker           | Source                        | Suggested next step       |
| ---                        | ---               | ---             | ---                       | ---                           | ---                       |
| TrackedSuspense            | component         | unknown         | usePathname (client-hook) | tracked-suspense.js:6         | Push the hook-using cl... |
| TeamDeploymentsLayout      | route-segment     | unknown         | unknown                   | layout.tsx:37                 | Inspect the nearest us... |
| Next.Metadata              | component         | unknown         | unknown                   | unknown                       | No primary blocker was... |

## Detail
  TrackedSuspense
    rendered by: TrackedSuspense > RootLayout > AppLayout
    environments: SSR
  TeamDeploymentsLayout
    suspenders unknown: thrown Promise (library using throw instead of use())

## Static (pre-rendered in shell)
  GeistProvider at .../geist-provider.tsx:80:9
  TrackedSuspense at ...
  ...

The Quick Reference table is the main overview — boundary, blocker, source, and suggested fix at a glance. The Detail section only appears for holes that have extra info (owner chains, environments, secondary blockers) not already in the table.

errors doesn't report while locked. If the shell looks wrong (empty, bailed to CSR), unlock and goto the page normally, then run errors. Don't debug blind under the lock.

Full bailout (scrollHeight = 0). When PPR bails out completely, unlock returns just "unlocked" with no shell analysis — there are no boundaries to report. In this case, unlock, goto the page normally, then use errors and logs to find the root cause.

tree

Full React component tree — every component on the page with its hierarchy, like the Components panel in React DevTools.

$ next-browser tree
# React component tree
# Columns: depth id parent name [key=...]
# Use `tree <id>` for props/hooks/state. IDs valid until next navigation.

0 38167 - Root
1 38168 38167 HeadManagerContext.Provider
2 38169 38168 Root
...
224 46375 46374 DeploymentsProvider
226 46506 46376 DeploymentsTable

tree <id>

Inspect one component: ancestor path, props, hooks, state, source location (source-mapped to original file).

$ next-browser tree 46375
path: Root > ... > Prerender(TeamDeploymentsPage) > Prerender(FullHeading) > Prerender(TrackedSuspense) > Suspense > DeploymentsProvider
DeploymentsProvider #46375
props:
  children: [<Lazy />, <Lazy />, <span />, <Lazy />, <Lazy />]
hooks:
  IsMobile: undefined (1 sub)
  Router: undefined (2 sub)
  DeploymentListScope: undefined (1 sub)
  User: undefined (4 sub)
  Team: undefined (4 sub)
  ...
  DeploymentsInfinite: undefined (12 sub)
source: app/(dashboard)/[teamSlug]/(team)/~/deployments/_parts/context.tsx:180:10

IDs are valid until navigation. Re-run tree after goto/push.

viewport [WxH]

Show or set the browser viewport size. Useful for testing responsive layouts.

$ next-browser viewport
1440x900

$ next-browser viewport 375x812
viewport set to 375x812

Once set, the viewport stays fixed across navigations. window.resizeTo() via eval is a no-op in Playwright — always use this command to change dimensions.

screenshot

Full-page PNG to a temp file. Returns the path. Read with the Read tool.

$ next-browser screenshot
/var/folders/.../next-browser-1772770369495.png

Don't narrate what the screenshot shows — the user can see the browser. State your conclusion or next action, not a description of the page.

Prefer snapshot over screenshot when you need to understand what's on the page or decide what to interact with. snapshot returns structured, semantic data (roles, names, state) that you can act on directly — screenshots are pixels you have to interpret. Use screenshot only when visual layout matters (CSS issues, verifying appearance, PPR shell inspection).

snapshot

Snapshot the page's accessibility tree — the semantic structure a screen reader sees — with [ref=eN] markers on every interactive element. Use this to discover what's on the page before clicking.

$ next-browser snapshot
- navigation "Main"
  - link "Home" [ref=e0]
  - link "Dashboard" [ref=e1]
- main
  - heading "Settings"
  - tablist
    - tab "General" [ref=e2] (selected)
    - tab "Security" [ref=e3]
  - region "Profile"
    - textbox "Username" [ref=e4]
    - button "Save" [ref=e5]

The tree shows headings, landmarks (navigation, main, region), and state (selected, checked, expanded, disabled) so you understand page layout, not just a flat element list.

Refs are ephemeral — they reset on every snapshot call and are invalid after navigation. Re-run snapshot after goto/push.

click <ref|text|selector>

Click an element using real pointer events (pointerdown → mousedown → pointerup → mouseup → click). This works with libraries that ignore synthetic .click() (Radix UI, Headless UI, etc.).

Three ways to target:

Input Example How it resolves

Ref from tree click e3 Looks up role+name from last snapshot

Plain text click "Security" Playwright text=Security selector

Playwright selector click "#submit-btn" Used as-is (CSS, role=, etc.)

Recommended workflow: run snapshot first, then click eN. Refs are the most reliable — they resolve via ARIA role+name, so they work even when elements have no stable CSS selector.

$ next-browser snapshot
- tablist
  - tab "General" [ref=e0] (selected)
  - tab "Security" [ref=e1]
$ next-browser click e1
clicked
$ next-browser snapshot
- tablist
  - tab "General" [ref=e0]
  - tab "Security" [ref=e1] (selected)

fill <ref|selector> <value>

Fill a text input or textarea. Clears existing content, then types the new value — dispatches all the events React and other frameworks expect.

$ next-browser snapshot
- textbox "Username" [ref=e4]
$ next-browser fill e4 "judegao"
filled

eval [ref] <script> · eval [ref] --file <path> · eval -

Run JS in page context. Returns the result as JSON.

With a ref, the script receives the DOM element as its argument — useful for inspecting a snapshot node or bridging to React internals:

$ next-browser eval e0 'el => el.tagName'
"BUTTON"

$ next-browser eval e0 'el => {
  const key = Object.keys(el).find(k => k.startsWith("__reactFiber$"));
  if (!key) return null;
  let fiber = el[key];
  while (fiber && typeof fiber.type !== "function") fiber = fiber.return;
  return fiber?.type?.displayName || fiber?.type?.name || null;
}'
"LoginButton"

For simple one-liners (no ref), pass the script inline:

$ next-browser eval 'document.title'
"Deployments – Vercel"

$ next-browser eval 'document.querySelectorAll("a[href]").length'
47

For multi-line or quote-heavy scripts, use --file (or -f) to avoid shell quoting issues entirely:

cat > /tmp/nb-eval.js << 'SCRIPT'
(() => {
  // your JS here — no shell escaping needed
  return someResult;
})()
SCRIPT
next-browser eval --file /tmp/nb-eval.js

You can also pipe via stdin: echo 'document.title' | next-browser eval -

Use this to read the Next.js error overlay (it's in shadow DOM): next-browser eval 'document.querySelector("nextjs-portal")?.shadowRoot?.querySelector("[data-nextjs-dialog]")?.textContent'

eval runs synchronously in page context — top-level await is not supported. Wrap in an async IIFE if you need to await: next-browser eval '(async () => { ... })()'.

errors

Build and runtime errors for the current page.

$ next-browser errors
{
  "configErrors": [],
  "sessionErrors": [
    {
      "url": "/vercel/~/deployments",
      "buildError": null,
      "r

...

User Reviews (0)

Write a Review

Effect
Usability
Docs
Compatibility

No reviews yet

Statistics

Installs4.0K
Rating4.4 / 5.0
Version
Updated2026年5月23日
Comparisons1

User Rating

4.4(43)
5
35%
4
51%
3
14%
2
0%
1
0%

Rate this Skill

0.0

Compatible Platforms

🔧Claude Code

Timeline

Created2026年3月20日
Last Updated2026年5月23日