<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Steve Zhang</title>
    <description>The latest articles on Forem by Steve Zhang (@stevez).</description>
    <link>https://forem.com/stevez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2834283%2F2ba91b92-bbe9-43af-be54-042ef521c0a7.png</url>
      <title>Forem: Steve Zhang</title>
      <link>https://forem.com/stevez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/stevez"/>
    <language>en</language>
    <item>
      <title>I Built a VS Code Extension That Turns Playwright Into an Interactive REPL</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 02 Apr 2026 14:45:52 +0000</pubDate>
      <link>https://forem.com/stevez/i-built-a-vs-code-extension-that-turns-playwright-into-an-interactive-repl-5103</link>
      <guid>https://forem.com/stevez/i-built-a-vs-code-extension-that-turns-playwright-into-an-interactive-repl-5103</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkaxfzfbpl8ig8sqhm84t.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkaxfzfbpl8ig8sqhm84t.gif" alt="Playwright REPL VS Code Extension" width="2632" height="1436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Writing Playwright tests follows the same loop: write code, run it, wait for the browser, see it fail, tweak a locator, run again. Each cycle takes seconds. I wanted something more interactive — type a command, see the result immediately.&lt;/p&gt;

&lt;p&gt;So I built a VS Code extension that adds an interactive REPL, visual element picker, assertion builder, and recorder on top of Playwright's Test Explorer. Everything shares one persistent browser — no restarts between runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow: Record → Pick → Assert → Run
&lt;/h2&gt;

&lt;p&gt;The extension adds three panels to VS Code's bottom bar: &lt;strong&gt;REPL&lt;/strong&gt;, &lt;strong&gt;Locator&lt;/strong&gt;, and &lt;strong&gt;Assert&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Launch Browser
&lt;/h3&gt;

&lt;p&gt;Click "Launch Browser" in the Testing sidebar. Chrome opens and stays open between test runs — zero startup overhead on each run.&lt;/p&gt;

&lt;h3&gt;
  
  
  REPL Panel
&lt;/h3&gt;

&lt;p&gt;Type keyword commands directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pw&amp;gt; goto https://demo.playwright.dev/todomvc/
pw&amp;gt; fill "What needs to be done?" Buy groceries
pw&amp;gt; press Enter
pw&amp;gt; snapshot
pw&amp;gt; verify text 1 item left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of writing &lt;code&gt;page.locator('[placeholder="What needs to be done?"]').fill('Buy groceries')&lt;/code&gt;, you type &lt;code&gt;fill "What needs to be done?" Buy groceries&lt;/code&gt;. Same Playwright engine, simpler syntax.&lt;/p&gt;

&lt;p&gt;JavaScript works too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.todo-list li&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get command history (up/down arrows), inline screenshots, execution timing, and meta-commands like &lt;code&gt;.clear&lt;/code&gt; and &lt;code&gt;.history&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmd58l2vlzh5by89a0cw7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmd58l2vlzh5by89a0cw7.gif" alt="REPL Panel" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick Locator
&lt;/h3&gt;

&lt;p&gt;Click the pick arrow, hover over any element in the browser. The Locator panel shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The generated &lt;strong&gt;locator&lt;/strong&gt; (editable — modify and re-test)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;highlight toggle&lt;/strong&gt; to verify it targets the right element&lt;/li&gt;
&lt;li&gt;The element's &lt;strong&gt;ARIA snapshot&lt;/strong&gt; for accessibility inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more inspecting the DOM to find the right selector.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwf9s48ikp0qkpj7tsoo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwf9s48ikp0qkpj7tsoo.gif" alt="Locator Panel" width="3640" height="1210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Assert Builder
&lt;/h3&gt;

&lt;p&gt;This is my favorite feature. Pick an element, and the Assert Builder shows you Playwright matchers — &lt;strong&gt;smart-filtered by element type&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input field → &lt;code&gt;toHaveValue&lt;/code&gt;, &lt;code&gt;toBeEnabled&lt;/code&gt;, &lt;code&gt;toBeDisabled&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Text element → &lt;code&gt;toContainText&lt;/code&gt;, &lt;code&gt;toHaveText&lt;/code&gt;, &lt;code&gt;toBeVisible&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Checkbox → &lt;code&gt;toBeChecked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Any element → &lt;code&gt;toBeAttached&lt;/code&gt;, &lt;code&gt;toHaveAttribute&lt;/code&gt;, &lt;code&gt;toHaveCount&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;13 matchers total. Click &lt;strong&gt;Verify&lt;/strong&gt; to run the assertion against the live page. Green or red, instantly. Supports negation (&lt;code&gt;not&lt;/code&gt; checkbox) and editable expected values.&lt;/p&gt;

&lt;p&gt;No more guessing whether your assertion is correct before running the full test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjojzd4ouhacu9ccwpjz3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjojzd4ouhacu9ccwpjz3.gif" alt="Assert Builder" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Recorder
&lt;/h3&gt;

&lt;p&gt;Hit Record, interact with the browser normally. Every click, fill, and navigation is captured as Playwright JavaScript — ready to paste into your test file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrtsl2s9xyepeav4rvrt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrtsl2s9xyepeav4rvrt.gif" alt="Recorder" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Tests
&lt;/h3&gt;

&lt;p&gt;The Test Explorer runs your existing &lt;code&gt;playwright.config.ts&lt;/code&gt; tests. With "Show Browser" enabled, browser-only tests take a fast path: the script is sent directly to the browser via the extension bridge, bypassing worker startup, TypeScript compilation, and fixture setup entirely.&lt;/p&gt;

&lt;p&gt;Tests that need Node APIs (&lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, etc.) fall back to the standard Playwright runner with CDP browser reuse — still fast, just not instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The extension launches Chrome with &lt;code&gt;--remote-debugging-port&lt;/code&gt; and a companion Chrome extension (Dramaturg) sideloaded. The REPL, Test Explorer, Recorder, and Picker all share this single browser instance via CDP.&lt;/p&gt;

&lt;p&gt;Each keyword command maps directly to a Playwright API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"click"     → locator.click()
"goto"      → page.goto()
"fill"      → locator.fill()
"snapshot"  → accessibility tree via CDP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword syntax is just a thin layer on top of the Playwright API — same engine, same locator resolution, same browser automation. No MCP, no daemon, no external process.&lt;/p&gt;

&lt;p&gt;For the bridge fast path, the test script is bundled with esbuild and evaluated inside the browser context where &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;expect&lt;/code&gt;, and all Playwright globals are already available. The result comes back via WebSocket. No test runner process involved at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser reuse: one browser for everything
&lt;/h2&gt;

&lt;p&gt;This is the architectural decision that makes the whole thing work. Instead of launching a new browser per test run (the Playwright default), the extension keeps one browser open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;REPL&lt;/strong&gt; sends commands to it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Explorer&lt;/strong&gt; runs tests in it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recorder&lt;/strong&gt; captures interactions from it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picker&lt;/strong&gt; inspects elements in it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same browser, same context, same cookies. Switch between REPL exploration and test execution seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built on Playwright Test for VS Code
&lt;/h2&gt;

&lt;p&gt;The extension is built on top of Microsoft's official &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright" rel="noopener noreferrer"&gt;Playwright Test for VS Code&lt;/a&gt; extension (Apache 2.0). Both can coexist — you keep the official one installed and use this alongside it. Same &lt;code&gt;playwright.config.ts&lt;/code&gt;, same test files, same Test Explorer interface.&lt;/p&gt;

&lt;p&gt;The added layers (REPL, Picker, Assert Builder, Recorder) are what turns it from a test runner into an interactive development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;strong&gt;"Playwright REPL"&lt;/strong&gt; from the &lt;a href="https://marketplace.visualstudio.com/items?itemName=playwright-repl.playwright-repl-vscode" rel="noopener noreferrer"&gt;VS Code Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open a project with a &lt;code&gt;playwright.config.ts&lt;/code&gt; — or clone the &lt;a href="https://github.com/stevez/playwright-examples" rel="noopener noreferrer"&gt;demo repo&lt;/a&gt; to try it immediately&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Launch Browser&lt;/strong&gt; in the Testing sidebar&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;REPL&lt;/strong&gt; panel in the bottom bar and type &lt;code&gt;goto https://example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Pick Locator&lt;/strong&gt; to inspect elements, &lt;strong&gt;Assert Builder&lt;/strong&gt; to verify values, and &lt;strong&gt;Record&lt;/strong&gt; to capture interactions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Requirements: VS Code 1.93+, Node.js 18+, &lt;code&gt;@playwright/test&lt;/code&gt; 1.58+ in your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm focused on making the record → assert → test loop as tight as possible. If you have ideas or find bugs, the repo is at &lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;github.com/stevez/playwright-repl&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=playwright-repl.playwright-repl-vscode" rel="noopener noreferrer"&gt;VS Code Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stevez/playwright-examples" rel="noopener noreferrer"&gt;Demo repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>playwright</category>
      <category>vscode</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Fast Can Browser Automation Be? I Benchmarked 3 Approaches</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Fri, 20 Mar 2026 03:12:30 +0000</pubDate>
      <link>https://forem.com/stevez/how-fast-can-browser-automation-be-we-benchmarked-3-approaches-54n5</link>
      <guid>https://forem.com/stevez/how-fast-can-browser-automation-be-we-benchmarked-3-approaches-54n5</guid>
      <description>&lt;p&gt;Most browser automation tools run outside the browser — connecting via CDP over a WebSocket, issuing commands, and waiting for responses. What if Playwright ran &lt;em&gt;inside&lt;/em&gt; the browser instead?&lt;/p&gt;

&lt;p&gt;We benchmarked three ways to run the same Playwright commands and the results were dramatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;We ran the same commands on &lt;a href="https://demo.playwright.dev/todomvc" rel="noopener noreferrer"&gt;TodoMVC&lt;/a&gt; across three modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/strong&gt; — Playwright's official CLI. Each command is a separate process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;playwright-repl&lt;/code&gt; standalone&lt;/strong&gt; — Our CLI in default mode. A persistent REPL — start once, run many commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;playwright-repl&lt;/code&gt; bridge&lt;/strong&gt; — Our CLI connected to the &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg&lt;/a&gt; Chrome extension. Playwright runs inside Chrome via &lt;a href="https://github.com/ruifigueira/playwright-crx" rel="noopener noreferrer"&gt;playwright-crx&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Standalone&lt;/th&gt;
&lt;th&gt;Bridge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;211ms&lt;/td&gt;
&lt;td&gt;4ms&lt;/td&gt;
&lt;td&gt;14ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;click&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,538ms&lt;/td&gt;
&lt;td&gt;1,046ms&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,240ms&lt;/td&gt;
&lt;td&gt;1,050ms&lt;/td&gt;
&lt;td&gt;28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,236ms&lt;/td&gt;
&lt;td&gt;1,036ms&lt;/td&gt;
&lt;td&gt;7ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;press&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,216ms&lt;/td&gt;
&lt;td&gt;1,033ms&lt;/td&gt;
&lt;td&gt;11ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,220ms&lt;/td&gt;
&lt;td&gt;1,010ms&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;screenshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;274ms&lt;/td&gt;
&lt;td&gt;107ms&lt;/td&gt;
&lt;td&gt;129ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tab-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;217ms&lt;/td&gt;
&lt;td&gt;4ms&lt;/td&gt;
&lt;td&gt;4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cookie-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;201ms&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Going On?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;playwright-cli&lt;/code&gt;: ~200ms baseline + action overhead
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;playwright-cli&lt;/code&gt; command spawns a new Node.js process, connects to the running browser session, executes the command, and exits. That's ~200ms before anything happens. Actions like &lt;code&gt;click&lt;/code&gt; and &lt;code&gt;fill&lt;/code&gt; add another ~1,000ms of Playwright auto-waiting on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone: fast queries, slow actions
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;playwright-repl&lt;/code&gt; starts once and keeps the connection open — no per-command startup cost. Queries like &lt;code&gt;snapshot&lt;/code&gt; and &lt;code&gt;tab-list&lt;/code&gt; are instant (3-4ms). But actions still take ~1,000ms because they go through the same external CDP pipeline: Node.js → WebSocket → Chrome → execute → wait for navigation/network idle → return.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bridge: fast everything
&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. In bridge mode, &lt;code&gt;playwright-repl&lt;/code&gt; connects to the Dramaturg Chrome extension, which runs Playwright &lt;em&gt;inside&lt;/em&gt; Chrome's service worker via &lt;code&gt;playwright-crx&lt;/code&gt;. There's no external CDP connection — everything happens in-process.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;click&lt;/code&gt; that takes 1,046ms in standalone takes &lt;strong&gt;30ms&lt;/strong&gt; in bridge mode. That's &lt;strong&gt;35x faster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? A typical &lt;code&gt;click&lt;/code&gt; involves many CDP round-trips under the hood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wait for element to be visible (poll via CDP)&lt;/li&gt;
&lt;li&gt;Wait for element to be enabled (poll via CDP)&lt;/li&gt;
&lt;li&gt;Scroll element into view (CDP)&lt;/li&gt;
&lt;li&gt;Calculate click coordinates (CDP)&lt;/li&gt;
&lt;li&gt;Perform the click (CDP)&lt;/li&gt;
&lt;li&gt;Wait for potential navigation (CDP events)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step adds latency when going through an external WebSocket. In bridge mode, each internal call is microseconds — so even a dozen steps stay under 30ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Queries Are Fast Everywhere
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;snapshot&lt;/code&gt; is 4ms in standalone and 14ms in bridge. Both fast. Why?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;snapshot&lt;/code&gt; is a single CDP call — walk the accessibility tree, return the result. One round-trip is fast regardless of whether it's external or in-process. The difference only shows up when commands involve &lt;em&gt;many&lt;/em&gt; CDP round-trips, which is what actions do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-World Impact
&lt;/h2&gt;

&lt;p&gt;For interactive use, the difference is feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/strong&gt;: type a command, wait 1-2 seconds, see result → feels sluggish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standalone REPL&lt;/strong&gt;: queries are instant, actions take ~1 second → usable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridge mode&lt;/strong&gt;: everything feels instant → flows like a conversation with the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For AI agents running via MCP, the difference is throughput. An agent exploring a page might run 50-100 commands. At 1 second per action, that's 1-2 minutes of waiting. At 30ms per action, it's 3 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; playwright-repl

&lt;span class="c"&gt;# Standalone mode&lt;/span&gt;
playwright-repl &lt;span class="nt"&gt;--headed&lt;/span&gt;

&lt;span class="c"&gt;# Bridge mode (requires Dramaturg extension)&lt;/span&gt;
playwright-repl &lt;span class="nt"&gt;--bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg from the Chrome Web Store&lt;/a&gt; for bridge mode.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;GitHub — playwright-repl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Chrome Web Store — Dramaturg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/playwright-repl" rel="noopener noreferrer"&gt;npm — playwright-repl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>playwright</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Let AI Control Your Real Browser — Not a Throwaway One</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 19 Mar 2026 21:57:38 +0000</pubDate>
      <link>https://forem.com/stevez/let-ai-control-your-real-browser-not-a-throwaway-one-3pma</link>
      <guid>https://forem.com/stevez/let-ai-control-your-real-browser-not-a-throwaway-one-3pma</guid>
      <description>&lt;p&gt;Most browser MCP servers launch a separate browser instance. Clean slate, no cookies, no auth. Want to automate something behind a login? Re-authenticate every time. Want to test your actual session state? Too bad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the AI could just use the browser you already have open?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's what &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg&lt;/a&gt; + &lt;code&gt;@playwright-repl/mcp&lt;/code&gt; does. The AI controls your real Chrome tabs — your cookies, your sessions, your localStorage — all intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude (or any MCP client)
  ↕ MCP (stdio)
playwright-repl MCP server
  ↕ WebSocket
Dramaturg Chrome extension
  ↕ CDP / chrome.debugger
Your real Chrome tabs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference: &lt;strong&gt;Playwright runs inside the browser&lt;/strong&gt;, not outside it. The extension uses &lt;a href="https://github.com/ruifigueira/playwright-crx" rel="noopener noreferrer"&gt;playwright-crx&lt;/a&gt; to run the full Playwright API directly in Chrome's service worker. No Node.js relay, no separate browser process.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No re-authentication&lt;/strong&gt; — Gmail, Notion, your internal tools — already logged in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expect()&lt;/code&gt; assertions&lt;/strong&gt; — something no other browser MCP server supports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You see everything&lt;/strong&gt; — the AI works in your actual tabs, in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Just two tools
&lt;/h2&gt;

&lt;p&gt;While Playwright's official MCP server exposes ~70 tools, &lt;code&gt;@playwright-repl/mcp&lt;/code&gt; exposes just &lt;strong&gt;two&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run_command&lt;/code&gt;&lt;/strong&gt; — execute a single command (keyword or JavaScript)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run_script&lt;/code&gt;&lt;/strong&gt; — run a multi-line script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI figures out what to do with them. Here's what a command looks like:&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyword syntax (&lt;code&gt;.pw&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;snapshot                    → accessibility tree
goto https://example.com    → navigate
click "Sign in"             → click by text
fill "Email" "user@test.com"  → fill a form field
verify-text "Welcome"       → assert text is visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Playwright API (JavaScript)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sign in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both work through the same &lt;code&gt;run_command&lt;/code&gt; tool. The extension auto-detects the format.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Agents that actually verify their work
&lt;/h2&gt;

&lt;p&gt;The MCP package includes four agents that go beyond "generate and hope":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planner&lt;/strong&gt; — Give it a URL and a goal. It takes snapshots, maps out the page structure, and produces a step-by-step workflow plan with the exact labels and text it found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generator&lt;/strong&gt; — Takes a plan and turns it into a working script. The key word is &lt;em&gt;working&lt;/em&gt; — it runs every command in the real browser, assembles the script, executes the full thing via &lt;code&gt;run_script&lt;/code&gt;, and iterates until it passes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Healer&lt;/strong&gt; — Give it a broken script. Wrong selectors, changed page structure, timing issues. It runs the script, diagnoses failures with snapshots, fixes them, and re-runs until all lines pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Converter&lt;/strong&gt; — Translates between &lt;code&gt;.pw&lt;/code&gt; keyword syntax and JavaScript Playwright API. Produces idiomatic output — proper locator strategies, extracted variables, chained methods.&lt;/p&gt;

&lt;p&gt;Every agent &lt;strong&gt;must run the script before outputting it&lt;/strong&gt;. No "here's what should work" — it either passes or the agent keeps fixing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @playwright-repl/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg from the Chrome Web Store&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"playwright-repl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright-repl-mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add playwright-repl playwright-repl-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Install the AI agents (optional)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/agents
&lt;span class="nb"&gt;cp &lt;/span&gt;node_modules/@playwright-repl/mcp/agents/&lt;span class="k"&gt;*&lt;/span&gt;.agent.md .claude/agents/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. That's it
&lt;/h3&gt;

&lt;p&gt;The extension connects automatically. No side panel needed. Just make sure Chrome is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example prompts
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Go to &lt;a href="https://demo.playwright.dev/todomvc" rel="noopener noreferrer"&gt;https://demo.playwright.dev/todomvc&lt;/a&gt;, add three todos, complete one, and verify the Active filter only shows the remaining two.&lt;/p&gt;

&lt;p&gt;@playwright-repl-planner explore &lt;a href="https://demo.playwright.dev/todomvc" rel="noopener noreferrer"&gt;https://demo.playwright.dev/todomvc&lt;/a&gt; and plan a test for adding and completing todos&lt;/p&gt;

&lt;p&gt;@playwright-repl-generator create a .pw script that adds 3 todos, completes one, and verifies the Active filter&lt;/p&gt;

&lt;p&gt;@playwright-repl-healer this script is failing on line 5 — the selector changed after a site update: [paste script]&lt;/p&gt;

&lt;p&gt;@playwright-repl-converter convert this .pw script to JavaScript: [paste script]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Human + AI, side by side
&lt;/h2&gt;

&lt;p&gt;The extension isn't just a bridge for the AI — it's a full REPL you can use alongside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch commands execute&lt;/strong&gt; in real time as the AI works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intervene manually&lt;/strong&gt; — type &lt;code&gt;snapshot&lt;/code&gt; to check state, or navigate to a different page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record your own interactions&lt;/strong&gt; — click Record, interact with the page, get &lt;code&gt;.pw&lt;/code&gt; commands or JS code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch tabs&lt;/strong&gt; — the toolbar dropdown lets you redirect both the AI and your commands to any open tab&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  vs. other browser MCP servers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;playwright-repl&lt;/th&gt;
&lt;th&gt;Playwright MCP&lt;/th&gt;
&lt;th&gt;Playwriter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MCP tools&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~70&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your real session&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright inside browser&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;expect()&lt;/code&gt; assertions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Playwright API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human REPL alongside AI&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; snapshot
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; click &lt;span class="s2"&gt;"Sign in"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fill &lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="s2"&gt;"user@test.com"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; verify-text &lt;span class="s2"&gt;"Dashboard"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your browser. Your sessions. AI that verifies its own work.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Chrome Web Store — Dramaturg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;GitHub — playwright-repl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@playwright-repl/mcp" rel="noopener noreferrer"&gt;npm — @playwright-repl/mcp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>playwright</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Put Playwright Inside a Chrome Side Panel — Console, Recorder, and Debugger Included</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Mon, 16 Mar 2026 13:57:28 +0000</pubDate>
      <link>https://forem.com/stevez/i-put-playwright-inside-a-chrome-side-panel-console-recorder-and-debugger-included-4a02</link>
      <guid>https://forem.com/stevez/i-put-playwright-inside-a-chrome-side-panel-console-recorder-and-debugger-included-4a02</guid>
      <description>&lt;p&gt;When I started building &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg&lt;/a&gt;, I just wanted a quick way to run Playwright commands without setting up a Node.js project. Type &lt;code&gt;click "Submit"&lt;/code&gt;, see it happen. Simple.&lt;/p&gt;

&lt;p&gt;But then I kept going. And going. And now it has a full JS debugger — breakpoints, step-through, scope inspection — all running inside a Chrome side panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Dramaturg?
&lt;/h2&gt;

&lt;p&gt;It's a Chrome extension that gives you a Playwright-powered REPL directly in your browser. No backend server, no IDE, no &lt;code&gt;npx playwright test&lt;/code&gt;. Just open the side panel and start automating.&lt;/p&gt;

&lt;p&gt;It has two input modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword mode (&lt;code&gt;.pw&lt;/code&gt;)&lt;/strong&gt; — human-friendly commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;goto https://demo.playwright.dev/todomvc
fill "What needs to be done?" "Buy groceries"
verify-text "1 item left"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JavaScript mode&lt;/strong&gt; — full Playwright API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both modes run against &lt;strong&gt;your real browser session&lt;/strong&gt; — cookies, auth, extensions, everything. This is the biggest difference from tools like Playwright MCP that launch isolated browser instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debugger that nobody asked for (but I needed)
&lt;/h2&gt;

&lt;p&gt;Once I had JS mode working, I found myself writing multi-line scripts and wanting to step through them. So I built a debugger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set breakpoints&lt;/strong&gt; by clicking the gutter. Hit &lt;strong&gt;Debug&lt;/strong&gt; instead of Run. The script pauses, and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step Over / Step Into / Step Out / Continue&lt;/strong&gt; — floating toolbar, just like VS Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables tab&lt;/strong&gt; — auto-expands local, block, closure, and script scope variables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expandable object tree&lt;/strong&gt; — arrays, objects, Maps, Promises — drill into any value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All powered by Chrome's &lt;code&gt;Debugger&lt;/code&gt; API (CDP). The extension creates a debug session against its own service worker, sets breakpoints, and catches &lt;code&gt;Debugger.paused&lt;/code&gt; events. No external debugger needed.&lt;/p&gt;

&lt;p&gt;Here's what it looks like paused inside a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// ← breakpoint here&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Variables tab shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;▾ Local&lt;/span&gt;
    &lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World"&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;World!"&lt;/span&gt;
&lt;span class="s"&gt;▾ Script&lt;/span&gt;
    &lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The extension runs &lt;a href="https://github.com/ruifigueira/playwright-crx" rel="noopener noreferrer"&gt;&lt;code&gt;playwright-crx&lt;/code&gt;&lt;/a&gt; in a service worker — this is a Chromium-specific build of Playwright that uses &lt;code&gt;chrome.debugger&lt;/code&gt; instead of CDP WebSocket. Commands execute directly in the browser process.&lt;/p&gt;

&lt;p&gt;For the debugger, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User clicks Debug → extension creates a CDP debug session targeting its own service worker&lt;/li&gt;
&lt;li&gt;Breakpoints are set via &lt;code&gt;Debugger.setBreakpoint&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Code executes via &lt;code&gt;Runtime.evaluate&lt;/code&gt; with &lt;code&gt;awaitPromise: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Debugger.paused&lt;/code&gt; event fires → extension reads &lt;code&gt;callFrames[0].scopeChain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scope variables displayed in the Variables tab via &lt;code&gt;Runtime.getProperties&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key trick: the script runs in the &lt;strong&gt;service worker context&lt;/strong&gt;, where &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;expect&lt;/code&gt;, and &lt;code&gt;crxApp&lt;/code&gt; are globals. So &lt;code&gt;await page.click('button')&lt;/code&gt; actually drives the attached browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console — Chrome DevTools, but for Playwright
&lt;/h2&gt;

&lt;p&gt;The console works like Chrome DevTools — type a command, see the result inline. But instead of raw JavaScript, you can use Playwright's keyword commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the page's accessibility tree as an &lt;strong&gt;expandable tree view&lt;/strong&gt; — click to drill into nodes, no more scrolling through walls of text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;goto https://example.com
&lt;span class="go"&gt;  Navigated to "Example Domain"
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;await page.title&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;  "Example Domain"
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;await page.evaluate&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; document.querySelectorAll&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.length&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;  1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console auto-detects what you're typing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;click "Submit"&lt;/code&gt; → runs as a &lt;code&gt;.pw&lt;/code&gt; keyword command&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await page.locator('h1').textContent()&lt;/code&gt; → runs as Playwright API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results show as &lt;strong&gt;expandable object trees&lt;/strong&gt; — arrays, objects, Maps, Promises — just like Chrome DevTools. Click to expand nested properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recorder — watch commands write themselves
&lt;/h2&gt;

&lt;p&gt;Click the &lt;strong&gt;Record&lt;/strong&gt; button, then interact with the page normally. Every click, form fill, and navigation is captured and written to the editor in real time.&lt;/p&gt;

&lt;p&gt;Recording works in both modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;.pw&lt;/code&gt; mode → generates keyword commands: &lt;code&gt;click "Submit"&lt;/code&gt;, &lt;code&gt;fill "Email" "test@example.com"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In JS mode → generates Playwright API: &lt;code&gt;await page.getByRole('button', { name: 'Submit' }).click()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop recording, clean up the script if needed, hit Run. Instant test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick Element — point and click locators
&lt;/h2&gt;

&lt;p&gt;Finding the right selector is half the battle. Click the &lt;strong&gt;Pick Element&lt;/strong&gt; button (crosshair icon), then hover over any element on the page. You'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;blue highlight overlay&lt;/strong&gt; around the element&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click to pick it. The console shows locators and assertions in both formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;locator&lt;/span&gt;
  &lt;span class="s"&gt;js&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;await page.getByRole('link', { name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Get&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;started'&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;).highlight();&lt;/span&gt;
  &lt;span class="s"&gt;pw&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;highlight link "Get started"&lt;/span&gt;

&lt;span class="s"&gt;assertion&lt;/span&gt;
  &lt;span class="s"&gt;js&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;await expect(page.getByRole('link', { name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Get&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;started'&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)).toContainText('Get started');&lt;/span&gt;
  &lt;span class="s"&gt;pw&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;verify-element link "Get started"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy whichever format you need — ready to paste into your script or test.&lt;/p&gt;

&lt;h2&gt;
  
  
  More features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot tree&lt;/strong&gt; — &lt;code&gt;snapshot&lt;/code&gt; renders the accessibility tree as an expandable tree view (not raw text).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocompletion&lt;/strong&gt; — ghost-text suggestions for both &lt;code&gt;.pw&lt;/code&gt; commands and Playwright API methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side panel or popup&lt;/strong&gt; — opens as a side panel by default; switch to popup window in Options if you prefer a floating window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server&lt;/strong&gt; — pair with &lt;code&gt;@playwright-repl/mcp&lt;/code&gt; to let Claude or other AI agents drive your browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just use Playwright Test?
&lt;/h2&gt;

&lt;p&gt;You absolutely should for production tests. Dramaturg is for the &lt;strong&gt;exploration phase&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quickly test a selector before putting it in your test suite&lt;/li&gt;
&lt;li&gt;Debug a flaky test by stepping through it interactively&lt;/li&gt;
&lt;li&gt;Record a user flow and convert it to a test&lt;/li&gt;
&lt;li&gt;Let an AI agent generate and run tests on your real session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as Chrome DevTools Console, but with Playwright superpowers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Web Store&lt;/strong&gt;: &lt;a href="https://chromewebstore.google.com/detail/dramaturg/ppbkmncnmjkfppilnmplpokdfagobipa" rel="noopener noreferrer"&gt;Dramaturg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;playwright-repl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt;: &lt;code&gt;npm install -g playwright-repl&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>playwright</category>
      <category>extensions</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>playwright-repl: Browser Automation From Your Terminal, No Code Required</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Sun, 15 Feb 2026 03:56:40 +0000</pubDate>
      <link>https://forem.com/stevez/playwright-repl-browser-automation-from-your-terminal-no-code-required-43n7</link>
      <guid>https://forem.com/stevez/playwright-repl-browser-automation-from-your-terminal-no-code-required-43n7</guid>
      <description>&lt;p&gt;I've been using &lt;a href="https://github.com/microsoft/playwright-cli" rel="noopener noreferrer"&gt;playwright-cli&lt;/a&gt; — Microsoft's command-line tool that gives AI agents browser automation skills via the Playwright MCP daemon. It's brilliant for agents: one command, one process, one result. But for humans, every command spawns a brand-new Node.js process — connect to the daemon, send one message, disconnect, exit. That's 50–100ms overhead per command.&lt;/p&gt;

&lt;p&gt;I wanted something faster. Something interactive. Something designed for &lt;strong&gt;humans&lt;/strong&gt; instead of AI agents — a persistent session with instant feedback.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;playwright-repl&lt;/strong&gt; — a REPL that reuses playwright-cli's command vocabulary and MCP daemon architecture, but replaces the one-shot client with a persistent socket connection. Same wire protocol, same daemon, same browser commands — just a better interface for interactive use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it look like?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;playwright-repl &lt;span class="nt"&gt;--headed&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;goto https://demo.playwright.dev/todomvc/
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill &lt;span class="s2"&gt;"What needs to be done?"&lt;/span&gt; &lt;span class="s2"&gt;"Buy groceries"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;press Enter
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill &lt;span class="s2"&gt;"What needs to be done?"&lt;/span&gt; &lt;span class="s2"&gt;"Write tests"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;press Enter
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;check &lt;span class="s2"&gt;"Buy groceries"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;verify-text &lt;span class="s2"&gt;"1 item left"&lt;/span&gt;
&lt;span class="go"&gt;✓ Text "1 item left" is visible
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No imports. No &lt;code&gt;async/await&lt;/code&gt;. No &lt;code&gt;page.locator()&lt;/code&gt;. Just type what you want the browser to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use Playwright's codegen?
&lt;/h2&gt;

&lt;p&gt;Playwright's codegen is great for generating test code. But sometimes you don't want code — you want to &lt;strong&gt;explore&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging a login flow? Type commands and see what happens.&lt;/li&gt;
&lt;li&gt;Building a demo? Record the session and replay it.&lt;/li&gt;
&lt;li&gt;Running a smoke test in CI? Pipe a &lt;code&gt;.pw&lt;/code&gt; file and check the exit code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;playwright-repl sits between "clicking around manually" and "writing a full test suite." It's the exploratory middle ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text locators: just say what you see
&lt;/h2&gt;

&lt;p&gt;The thing I'm most proud of is text locators. Instead of inspecting elements for CSS selectors or waiting for a snapshot to get element refs, you just use the text you see on screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;click &lt;span class="s2"&gt;"Get Started"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill &lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill &lt;span class="s2"&gt;"Password"&lt;/span&gt; &lt;span class="s2"&gt;"secret123"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;click &lt;span class="s2"&gt;"Sign In"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;check &lt;span class="s2"&gt;"Remember me"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s2"&gt;"Country"&lt;/span&gt; &lt;span class="s2"&gt;"Japan"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, it tries multiple strategies — &lt;code&gt;getByText&lt;/code&gt;, &lt;code&gt;getByRole('button')&lt;/code&gt;, &lt;code&gt;getByRole('link')&lt;/code&gt;, &lt;code&gt;getByLabel&lt;/code&gt;, &lt;code&gt;getByPlaceholder&lt;/code&gt; — with a fallback chain. Case differences don't matter because role matching is case-insensitive.&lt;/p&gt;

&lt;p&gt;You can also use element refs from &lt;code&gt;snapshot&lt;/code&gt; output if you prefer precision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;snapshot
&lt;span class="go"&gt;- heading "todos" [ref=e1]
- textbox "What needs to be done?" [ref=e8]

&lt;/span&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;click e8
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill e8 &lt;span class="s2"&gt;"Buy groceries"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Record and replay
&lt;/h2&gt;

&lt;p&gt;This is where it gets really useful. Record your browser session as a &lt;code&gt;.pw&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.record smoke-test
&lt;span class="go"&gt;⏺ Recording to smoke-test.pw

&lt;/span&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;goto https://demo.playwright.dev/todomvc/
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fill &lt;span class="s2"&gt;"What needs to be done?"&lt;/span&gt; &lt;span class="s2"&gt;"Buy groceries"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;press Enter
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;verify-text &lt;span class="s2"&gt;"1 item left"&lt;/span&gt;
&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.save
&lt;span class="go"&gt;✓ Saved 4 commands to smoke-test.pw
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.pw&lt;/code&gt; file is just plain text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CI smoke test
goto https://demo.playwright.dev/todomvc/
fill "What needs to be done?" "Buy groceries"
press Enter
verify-text "1 item left"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replay it any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Headless (CI mode)&lt;/span&gt;
playwright-repl &lt;span class="nt"&gt;--replay&lt;/span&gt; smoke-test.pw &lt;span class="nt"&gt;--silent&lt;/span&gt;

&lt;span class="c"&gt;# With a visible browser&lt;/span&gt;
playwright-repl &lt;span class="nt"&gt;--replay&lt;/span&gt; smoke-test.pw &lt;span class="nt"&gt;--headed&lt;/span&gt;

&lt;span class="c"&gt;# Step through interactively&lt;/span&gt;
playwright-repl &lt;span class="nt"&gt;--replay&lt;/span&gt; smoke-test.pw &lt;span class="nt"&gt;--step&lt;/span&gt; &lt;span class="nt"&gt;--headed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These &lt;code&gt;.pw&lt;/code&gt; files are human-readable, diffable, and version-controllable. Commit them alongside your code. Run them in CI. Share them with teammates who don't write JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions built in
&lt;/h2&gt;

&lt;p&gt;No test framework needed. Verify state inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;verify-text &lt;span class="s2"&gt;"1 item left"&lt;/span&gt;
&lt;span class="go"&gt;✓ Text "1 item left" is visible

&lt;/span&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;verify-element heading &lt;span class="s2"&gt;"todos"&lt;/span&gt;
&lt;span class="go"&gt;✓ Element heading "todos" is visible

&lt;/span&gt;&lt;span class="gp"&gt;pw&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;verify-value &lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;
&lt;span class="go"&gt;✓ Value matches
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an assertion fails, you get a clear error — and in replay mode, the process exits with code 1. That's all CI needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  50+ commands, short aliases
&lt;/h2&gt;

&lt;p&gt;Every command has a short alias for quick typing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You type&lt;/th&gt;
&lt;th&gt;It does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g https://example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Navigate to URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accessibility tree snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c e5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Click element ref e5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f "Email" "me@test.com"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fill a form field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Press a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Take a screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vt "hello"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify text is visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;back&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go back in history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full list includes interaction (click, fill, type, press, hover, drag), inspection (snapshot, screenshot, eval, console, network), storage (cookies, localStorage, sessionStorage), tabs, dialogs, network routing, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh05771pxugwj5ynx9bv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh05771pxugwj5ynx9bv.png" alt="Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;playwright-repl stands on the shoulders of &lt;a href="https://github.com/microsoft/playwright-cli" rel="noopener noreferrer"&gt;playwright-cli&lt;/a&gt; and the Playwright MCP architecture. It reuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The MCP daemon&lt;/strong&gt; from &lt;code&gt;playwright@1.59+&lt;/code&gt; — browser launch, CDP communication, all 50+ tool handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The command vocabulary&lt;/strong&gt; from playwright-cli — &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;snapshot&lt;/code&gt;, &lt;code&gt;screenshot&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wire protocol&lt;/strong&gt; — newline-delimited JSON over Unix socket / Windows named pipe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The REPL is just a thin, persistent client that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parses your input (alias resolution + argument parsing)&lt;/li&gt;
&lt;li&gt;Sends a JSON message over the socket (identical format to playwright-cli)&lt;/li&gt;
&lt;li&gt;Displays the result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the socket stays open, there's zero startup overhead per command. The daemon doesn't care whether the message came from playwright-cli, the REPL, or an AI agent — the wire messages are identical. playwright-repl adds the human-friendly layer on top: text locators, recording, replay, assertions, and aliases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it in CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/smoke.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;smoke-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install chromium&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright-repl --replay tests/smoke.pw --silent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--silent&lt;/code&gt; flag suppresses the banner. The process exits 0 on success, 1 on failure. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; playwright-repl
npx playwright &lt;span class="nb"&gt;install
&lt;/span&gt;playwright-repl &lt;span class="nt"&gt;--headed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then type &lt;code&gt;goto https://demo.playwright.dev/todomvc/&lt;/code&gt; and start exploring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome DevTools extension
&lt;/h2&gt;

&lt;p&gt;We're also building &lt;strong&gt;playwright-repl-extension&lt;/strong&gt; — a Chrome DevTools panel that brings the same REPL experience into the browser. Open DevTools, switch to the "Playwright" tab, and type commands directly. It also includes an action recorder that captures your clicks and form fills as &lt;code&gt;.pw&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;The extension is standalone (no Node.js daemon needed) — it uses Chrome's &lt;code&gt;chrome.debugger&lt;/code&gt; API to drive the inspected page via CDP.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/stevez/playwright-repl-extension" rel="noopener noreferrer"&gt;stevez/playwright-repl-extension&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/playwright-repl" rel="noopener noreferrer"&gt;playwright-repl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/stevez/playwright-repl" rel="noopener noreferrer"&gt;stevez/playwright-repl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chrome extension: &lt;a href="https://github.com/stevez/playwright-repl-extension" rel="noopener noreferrer"&gt;stevez/playwright-repl-extension&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Inspired by: &lt;a href="https://github.com/microsoft/playwright-cli" rel="noopener noreferrer"&gt;microsoft/playwright-cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;playwright-repl is MIT licensed and works on Linux, macOS, and Windows. Contributions welcome!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>node</category>
    </item>
    <item>
      <title>E2E Coverage in Next.js: Dev Mode vs Production Mode</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 20:42:28 +0000</pubDate>
      <link>https://forem.com/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf</link>
      <guid>https://forem.com/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf</guid>
      <description>&lt;p&gt;&lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; supports collecting coverage from both &lt;code&gt;next dev&lt;/code&gt; and &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;. This article explains the differences and when to use each mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev Mode
&lt;/h2&gt;

&lt;p&gt;In dev mode, nextcov collects coverage directly from Next.js's webpack dev server.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Next.js dev server starts with &lt;code&gt;--inspect=9230&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Next.js spawns a &lt;strong&gt;worker process&lt;/strong&gt; on port 9231 (inspect port + 1)&lt;/li&gt;
&lt;li&gt;nextcov connects to the worker via CDP (Chrome DevTools Protocol)&lt;/li&gt;
&lt;li&gt;Coverage is collected using &lt;code&gt;Profiler.startPreciseCoverage()&lt;/code&gt; API&lt;/li&gt;
&lt;li&gt;Source maps are &lt;strong&gt;inline&lt;/strong&gt; (webpack eval) — no build artifacts needed&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Running Dev Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start Next.js dev server with inspector enabled&lt;/span&gt;
&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect=9230'&lt;/span&gt; npm run dev &amp;amp;

&lt;span class="c"&gt;# Run tests&lt;/span&gt;
npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dev Mode Characteristics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No build required&lt;/strong&gt; — start testing immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline source maps&lt;/strong&gt; — webpack eval scripts contain embedded source maps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDP port&lt;/strong&gt;: &lt;code&gt;cdpPort + 1&lt;/code&gt; (9231) — connects to the worker process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot reload&lt;/strong&gt; — changes are reflected without restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slower&lt;/strong&gt; — on-demand compilation adds overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client coverage&lt;/strong&gt; — Playwright connects to the browser via CDP&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Mode
&lt;/h2&gt;

&lt;p&gt;In production mode, nextcov uses Node.js native V8 coverage with pre-built bundles.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Build Next.js with source maps enabled (&lt;code&gt;E2E_MODE=true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Start the production server with &lt;code&gt;NODE_V8_COVERAGE&lt;/code&gt; and &lt;code&gt;--inspect&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;nextcov connects via CDP on port 9230&lt;/li&gt;
&lt;li&gt;Coverage is collected using Node.js native V8 coverage&lt;/li&gt;
&lt;li&gt;Source maps are &lt;strong&gt;external&lt;/strong&gt; (.map files) generated during build&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Running Production Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build with source maps&lt;/span&gt;
&lt;span class="nv"&gt;E2E_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Start server with coverage enabled&lt;/span&gt;
&lt;span class="nv"&gt;NODE_V8_COVERAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.v8-coverage &lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect=9230'&lt;/span&gt; npm start &amp;amp;

&lt;span class="c"&gt;# Run tests&lt;/span&gt;
npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Production Mode Characteristics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build required&lt;/strong&gt; — must run &lt;code&gt;next build&lt;/code&gt; first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External source maps&lt;/strong&gt; — .map files in build output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDP port&lt;/strong&gt;: &lt;code&gt;cdpPort&lt;/code&gt; (9230) — connects directly to the server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hot reload&lt;/strong&gt; — restart required for changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster&lt;/strong&gt; — pre-compiled bundles execute quickly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client coverage&lt;/strong&gt; — Playwright connects to the browser via CDP&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Dev Mode&lt;/th&gt;
&lt;th&gt;Production Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Command&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;next dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source Maps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inline (webpack eval)&lt;/td&gt;
&lt;td&gt;External (.map files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build Required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hot Reload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coverage Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CDP Profiler API&lt;/td&gt;
&lt;td&gt;Node.js native V8 coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDP Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cdpPort + 1&lt;/code&gt; (9231)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cdpPort&lt;/code&gt; (9230)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slower (compilation)&lt;/td&gt;
&lt;td&gt;Faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recommended For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Development iteration&lt;/td&gt;
&lt;td&gt;CI/CD, final coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Auto-Detection
&lt;/h2&gt;

&lt;p&gt;nextcov &lt;strong&gt;automatically detects&lt;/strong&gt; the mode — no configuration needed. Just use the same &lt;code&gt;globalSetup&lt;/code&gt; and &lt;code&gt;globalTeardown&lt;/code&gt; for both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 Auto-detecting server mode...
  Trying dev mode (worker port 9231)...
  ✓ Dev mode detected (webpack eval scripts found)
  ✓ Server coverage collection started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for production mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 Auto-detecting server mode...
  Trying dev mode (worker port 9231)...
  ⚠️ Failed to connect to CDP (dev mode): Error: connect ECONNREFUSED
  ℹ️ Production mode will be used (NODE_V8_COVERAGE + port 9230)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Same Output, Different Paths
&lt;/h2&gt;

&lt;p&gt;Both modes produce identical Istanbul-compatible output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;coverage/e2e/
├── coverage-final.json   # Merges with Vitest coverage
├── lcov.info             # For CI tools
└── index.html            # Interactive report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use dev mode during development for fast iteration&lt;/li&gt;
&lt;li&gt;Use production mode in CI for accurate final reports&lt;/li&gt;
&lt;li&gt;Merge both with Vitest coverage using &lt;code&gt;nextcov merge&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local development&lt;/td&gt;
&lt;td&gt;Dev&lt;/td&gt;
&lt;td&gt;&lt;code&gt;next dev --inspect=9230&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipeline&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;&lt;code&gt;next build &amp;amp;&amp;amp; next start --inspect=9230&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick coverage check&lt;/td&gt;
&lt;td&gt;Dev&lt;/td&gt;
&lt;td&gt;No build required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production validation&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Uses production bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both modes work seamlessly with nextcov — just run your tests and coverage is collected automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; — E2E coverage for Next.js with Playwright&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stevez/nextcov#development-mode-coverage" rel="noopener noreferrer"&gt;nextcov README: Dev Mode&lt;/a&gt; — Full documentation&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part 6 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;nextcov - Collecting Test Coverage for Next.js Server Components&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip"&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8"&gt;V8 Coverage vs Istanbul: Performance and Accuracy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2"&gt;V8 Coverage Limitations and How to Work Around Them&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f"&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode (this article)&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>playwright</category>
      <category>e2e</category>
    </item>
    <item>
      <title>How to Merge Vitest Unit, Component, and E2E Test Coverage</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 20:19:54 +0000</pubDate>
      <link>https://forem.com/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f</link>
      <guid>https://forem.com/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📝 UPDATE (January 2025):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As of &lt;code&gt;nextcov@1.1.0&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Directive stripping is now OFF by default&lt;/strong&gt; - Use &lt;code&gt;--strip-directives&lt;/code&gt; if you need it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="mailto:ast-v8-to-istanbul@0.3.10"&gt;ast-v8-to-istanbul@0.3.10&lt;/a&gt; fix&lt;/strong&gt; - Ternary and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; patterns in JSX now have proper branch coverage, so coverage merging should have fewer mismatches&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern React applications often have multiple test types running in different environments. Each produces its own coverage report. This article shows how to merge them into a single, accurate coverage report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This article assumes the following setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;React + Next.js / Vite / Webpack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit Tests&lt;/td&gt;
&lt;td&gt;Vitest (jsdom environment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component Tests&lt;/td&gt;
&lt;td&gt;Vitest Browser Mode + Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2E Tests&lt;/td&gt;
&lt;td&gt;Playwright + nextcov&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage Provider&lt;/td&gt;
&lt;td&gt;V8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Problem: Separate Coverage Reports
&lt;/h2&gt;

&lt;p&gt;A typical Next.js project might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;coverage/
├── unit/           # From vitest unit tests (jsdom)
├── component/      # From vitest browser tests
└── e2e/            # From playwright + nextcov
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory contains a &lt;code&gt;coverage-final.json&lt;/code&gt; file with coverage data for the same source files. But the coverage numbers don't add up correctly if you try to merge them naively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Naive Merging Fails
&lt;/h3&gt;

&lt;p&gt;Consider a simple component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/components/Input.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Vitest runs unit tests, it counts the &lt;code&gt;'use client'&lt;/code&gt; directive and &lt;code&gt;import&lt;/code&gt; statement as executable statements. When V8 coverage runs during E2E tests, these lines don't exist in the bundled code.&lt;/p&gt;

&lt;p&gt;If you merge these reports without accounting for this difference, you get inflated statement counts or incorrect coverage percentages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Separate Coverage Directories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Unit Test Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.mts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react-swc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tsconfigPaths&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-tsconfig-paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;tsconfigPaths&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./vitest.setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/__tests__/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.browser.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reportsDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./coverage/unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/__tests__/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.browser.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;reportsDirectory: './coverage/unit'&lt;/code&gt; — Output to a dedicated directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider: 'v8'&lt;/code&gt; — Use V8 coverage (not Istanbul)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reporter: ['json']&lt;/code&gt; — Must include &lt;code&gt;json&lt;/code&gt; to generate &lt;code&gt;coverage-final.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Component Test Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.component.config.mts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react-swc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tsconfigPaths&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-tsconfig-paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;playwright&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitest/browser-playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;tsconfigPaths&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./vitest.browser.setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.browser.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reportsDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./coverage/component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/__tests__/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.browser.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key differences from unit tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;include: ['src/**/*.browser.test.{ts,tsx}']&lt;/code&gt; — Different file pattern&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reportsDirectory: './coverage/component'&lt;/code&gt; — Separate output directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;browser.enabled: true&lt;/code&gt; — Runs in a real browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Package.json Scripts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run --config vitest.component.config.mts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run test:unit &amp;amp;&amp;amp; npm run test:component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage:merge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Directive Stripping Problem
&lt;/h2&gt;

&lt;p&gt;Before diving into merge tools, you need to understand why naive merging fails.&lt;/p&gt;

&lt;p&gt;When running tests in different environments, V8 coverage produces inconsistent statement counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vitest unit tests (jsdom)&lt;/strong&gt;: Counts &lt;code&gt;import&lt;/code&gt; statements and directives (&lt;code&gt;'use client'&lt;/code&gt;, &lt;code&gt;'use server'&lt;/code&gt;) as executable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vitest browser tests&lt;/strong&gt;: Also counts imports as executable statements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E coverage (bundled code)&lt;/strong&gt;: These statements don't exist in the bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without normalization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Vitest unit coverage (counts directives)
Input.tsx: 10 statements, 8 covered (80%)

# E2E coverage (no directives in bundle)
Input.tsx: 8 statements, 6 covered (75%)

# Naive merge (wrong!)
Input.tsx: 10 statements, 14 covered (140%?!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solution is to strip import statements and directives before merging, normalizing all sources to the same baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Use Vitest's Multi-Project Setup?
&lt;/h2&gt;

&lt;p&gt;You might think Vitest's &lt;a href="https://vitest.dev/guide/projects" rel="noopener noreferrer"&gt;multi-project setup&lt;/a&gt; could solve this — run unit and component tests as separate projects and let Vitest merge the coverage automatically.&lt;/p&gt;

&lt;p&gt;Unfortunately, there's currently a &lt;a href="https://github.com/vitest-dev/vitest/issues/9366" rel="noopener noreferrer"&gt;bug in Vitest&lt;/a&gt; where multi-project setups don't properly merge coverage reports. Until this is fixed, you need external tools to merge coverage from different test environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: vitest-coverage-merge (Vitest Only)
&lt;/h2&gt;

&lt;p&gt;For merging just Vitest unit and component test coverage, use &lt;a href="https://www.npmjs.com/package/vitest-coverage-merge" rel="noopener noreferrer"&gt;vitest-coverage-merge&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; vitest-coverage-merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vitest-coverage-merge coverage/unit coverage/component &lt;span class="nt"&gt;-o&lt;/span&gt; coverage/merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What vitest-coverage-merge Does
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loads coverage files&lt;/strong&gt; from each directory's &lt;code&gt;coverage-final.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalizes by stripping&lt;/strong&gt; ESM import statements and React/Next.js directives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligently merges&lt;/strong&gt; while preferring browser test structures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates reports&lt;/strong&gt; (HTML, LCOV, JSON)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This tool specifically addresses the environment-based coverage differences between jsdom and real browser tests — unlike Vitest's built-in &lt;code&gt;--merge-reports&lt;/code&gt; which only handles sharded test runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package.json Scripts (Vitest Only)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run --config vitest.component.config.mts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run test:unit &amp;amp;&amp;amp; npm run test:component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage:merge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest-coverage-merge coverage/unit coverage/component -o coverage/merged"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Option 2: nextcov merge (Including E2E)
&lt;/h2&gt;

&lt;p&gt;For merging Vitest coverage with E2E coverage from Playwright + nextcov, use &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov merge&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nextcov merge coverage/unit coverage/component coverage/e2e &lt;span class="nt"&gt;-o&lt;/span&gt; coverage/merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What nextcov merge Does
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loads coverage files&lt;/strong&gt; from each directory's &lt;code&gt;coverage-final.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally strips directives&lt;/strong&gt; (&lt;code&gt;'use client'&lt;/code&gt;, &lt;code&gt;'use server'&lt;/code&gt;, &lt;code&gt;import&lt;/code&gt; statements) when using &lt;code&gt;--strip-directives&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selects the best structure&lt;/strong&gt; for each file (prefers sources without directive inflation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merges execution counts&lt;/strong&gt; using a "max" strategy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates reports&lt;/strong&gt; (HTML, LCOV, JSON, text-summary)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Enabling Directive Stripping
&lt;/h3&gt;

&lt;p&gt;By default, nextcov merge does NOT strip directives. If you have coverage mismatches due to &lt;code&gt;'use client'&lt;/code&gt;, &lt;code&gt;'use server'&lt;/code&gt;, or &lt;code&gt;import&lt;/code&gt; statements (common when merging Vitest coverage with E2E coverage), you can enable stripping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nextcov merge coverage/unit coverage/component coverage/e2e &lt;span class="nt"&gt;--strip-directives&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding E2E Coverage
&lt;/h2&gt;

&lt;p&gt;For complete coverage, add E2E test coverage with nextcov:&lt;/p&gt;

&lt;h3&gt;
  
  
  Playwright Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextcovConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nextcov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextcov&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextcovConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cdpPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverage/e2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/__tests__/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.browser.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;reporters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lcov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./e2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./e2e/coverage-reporter.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Package.json Scripts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env E2E_MODE=true npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start:e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env E2E_MODE=true NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS=--inspect=9230 next start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run e2e:clean &amp;amp;&amp;amp; npm run e2e:run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"e2e:clean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rimraf coverage/e2e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"e2e:run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start-server-and-test start:e2e http://localhost:3000 playwright-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage:merge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;Article 1&lt;/a&gt; for detailed E2E coverage setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge Strategy Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Max Strategy (Default)
&lt;/h3&gt;

&lt;p&gt;For each coverage item (statement, branch, function), nextcov takes the maximum count across all sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unit tests:   line 10 executed 5 times
Component:    line 10 executed 3 times
E2E:          line 10 executed 2 times
────────────────────────────────────────
Merged:       line 10 executed 5 times (max)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is conservative — it reflects the highest observed coverage without double-counting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structure Selection
&lt;/h3&gt;

&lt;p&gt;When merging, nextcov must choose which source's "structure" to use (which statements exist, where branches are). It prefers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sources without L1:0 directive statements (E2E-style coverage)&lt;/li&gt;
&lt;li&gt;Sources with more coverage items (more complete analysis)&lt;/li&gt;
&lt;li&gt;Later sources when equal (E2E is typically last)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures the merged report doesn't show inflated statement counts from directive-tracking sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Example
&lt;/h2&gt;

&lt;p&gt;Here's a full working setup:&lt;/p&gt;

&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── src/
│   ├── components/
│   │   ├── Input.tsx
│   │   └── __tests__/
│   │       ├── Input.test.tsx          # Unit tests
│   │       └── Input.browser.test.tsx  # Component tests
├── e2e/
│   └── form.spec.ts                 # E2E tests
├── coverage/
│   ├── unit/
│   ├── component/
│   ├── e2e/
│   └── merged/
├── vitest.config.mts
├── vitest.component.config.mts
├── playwright.config.ts
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running Tests and Merging
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run all tests with coverage&lt;/span&gt;
npm run &lt;span class="nb"&gt;test&lt;/span&gt;:unit
npm run &lt;span class="nb"&gt;test&lt;/span&gt;:component
npm run e2e

&lt;span class="c"&gt;# Merge all coverage&lt;/span&gt;
npx nextcov merge coverage/unit coverage/component coverage/e2e &lt;span class="nt"&gt;-o&lt;/span&gt; coverage/merged

&lt;span class="c"&gt;# Or use the npm script&lt;/span&gt;
npm run coverage:merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 nextcov merge
   Inputs: coverage/unit, coverage/component, coverage/e2e
   Output: coverage/merged
   Reporters: html, lcov, json, text-summary
   Strip directives: no

   Loading: coverage/unit/coverage-final.json
   Loading: coverage/component/coverage-final.json
   Loading: coverage/e2e/coverage-final.json

=============================== Coverage summary ===============================
Statements   : 89.07% ( 595/668 )
Branches     : 78.06% ( 338/433 )
Functions    : 92.9% ( 131/141 )
Lines        : 88.71% ( 574/647 )
================================================================================

✅ Merged coverage report generated
   Output: coverage/merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choosing Report Formats
&lt;/h2&gt;

&lt;p&gt;By default, nextcov generates all report formats. You can customize this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only HTML and LCOV (for CI integration)&lt;/span&gt;
npx nextcov merge coverage/unit coverage/e2e &lt;span class="nt"&gt;--reporters&lt;/span&gt; html,lcov

&lt;span class="c"&gt;# Only JSON (for further processing)&lt;/span&gt;
npx nextcov merge coverage/unit coverage/e2e &lt;span class="nt"&gt;--reporters&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Available reporters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;html&lt;/code&gt; — Interactive HTML report&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lcov&lt;/code&gt; — LCOV format for CI tools (Codecov, Coveralls)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json&lt;/code&gt; — JSON format (&lt;code&gt;coverage-final.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text-summary&lt;/code&gt; — Console summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "No coverage-final.json found"
&lt;/h3&gt;

&lt;p&gt;Ensure your Vitest config includes &lt;code&gt;json&lt;/code&gt; in the reporters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// Must include 'json'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Coverage percentages seem wrong after merge
&lt;/h3&gt;

&lt;p&gt;This usually means directive stripping is needed. Check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are you using V8 coverage (&lt;code&gt;provider: 'v8'&lt;/code&gt;) in all sources?&lt;/li&gt;
&lt;li&gt;Do your source files have &lt;code&gt;'use client'&lt;/code&gt; or &lt;code&gt;'use server'&lt;/code&gt; directives?&lt;/li&gt;
&lt;li&gt;Try running with &lt;code&gt;--strip-directives&lt;/code&gt; to normalize the coverage data&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Some files missing from merged report
&lt;/h3&gt;

&lt;p&gt;Files only appear if they're in at least one source. If E2E tests don't exercise a file, it won't appear in E2E coverage. The merge will still include it from unit/component coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run test:unit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coverage/unit/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run test:component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coverage/component/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2E tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run e2e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coverage/e2e/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge all&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx nextcov merge coverage/unit coverage/component coverage/e2e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coverage/merged/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use separate &lt;code&gt;reportsDirectory&lt;/code&gt; for each test type&lt;/li&gt;
&lt;li&gt;Always include &lt;code&gt;json&lt;/code&gt; reporter to generate &lt;code&gt;coverage-final.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;vitest-coverage-merge&lt;/code&gt; for Vitest-only merging, or &lt;code&gt;nextcov merge&lt;/code&gt; to include E2E coverage&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;--strip-directives&lt;/code&gt; if you have directive mismatches between coverage sources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/vitest-coverage-merge" rel="noopener noreferrer"&gt;vitest-coverage-merge&lt;/a&gt; — Merge Vitest unit and component test coverage&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; — E2E coverage for Next.js with Playwright&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vitest.dev/guide/coverage.html" rel="noopener noreferrer"&gt;Vitest Coverage&lt;/a&gt; — Official Vitest coverage documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vitest.dev/guide/browser/" rel="noopener noreferrer"&gt;@vitest/browser&lt;/a&gt; — Browser testing with Vitest&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part 5 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;nextcov - Collecting Test Coverage for Next.js Server Components&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip"&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8"&gt;V8 Coverage vs Istanbul: Performance and Accuracy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2"&gt;V8 Coverage Limitations and How to Work Around Them&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage (this article)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf"&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>vitest</category>
      <category>react</category>
      <category>playwright</category>
    </item>
    <item>
      <title>V8 Coverage Limitations and How to Work Around Them</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 19:38:05 +0000</pubDate>
      <link>https://forem.com/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2</link>
      <guid>https://forem.com/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎉 UPDATE: This limitation has been FIXED!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As of &lt;code&gt;ast-v8-to-istanbul@0.3.10&lt;/code&gt;, V8 coverage now properly tracks branch coverage for ternary operators and logical AND patterns in JSX. The fix was in the &lt;code&gt;getSourceLines&lt;/code&gt; function that previously failed to correctly match sources in multi-source source maps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vitest users&lt;/strong&gt;: Update to the latest version (it uses ast-v8-to-istanbul internally)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nextcov users&lt;/strong&gt;: Update to v1.1.0+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eslint-plugin-v8-coverage&lt;/strong&gt;: No longer needed - you can remove it from your project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workarounds described in this article are no longer necessary, but remain valid coding patterns if you prefer them for readability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;V8 native coverage is powerful — it works with any bundler, has minimal overhead, and can collect coverage across multiple processes. But it has one specific limitation you need to understand when testing React applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JSX Blind Spots
&lt;/h2&gt;

&lt;p&gt;V8 coverage has blind spots for &lt;strong&gt;ternary operators and logical AND that return JSX inside &lt;code&gt;{}&lt;/code&gt;&lt;/strong&gt; (JSX expression containers):&lt;/p&gt;

&lt;h3&gt;
  
  
  Ternary Returning JSX
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginPrompt&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V8 doesn't recognize this as a branch at all — it reports 0 branches for this line. Even if your test only renders with &lt;code&gt;isLoggedIn={true}&lt;/code&gt;, V8 won't warn you that &lt;code&gt;&amp;lt;LoginPrompt /&amp;gt;&lt;/code&gt; was never rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logical AND Returning JSX
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ErrorDisplay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorMessage&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same as the ternary case — V8 reports 0 branches for this line. Even if your test only passes &lt;code&gt;error={null}&lt;/code&gt;, V8 won't warn you that &lt;code&gt;&amp;lt;ErrorMessage /&amp;gt;&lt;/code&gt; was never rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Works Correctly
&lt;/h3&gt;

&lt;p&gt;V8 tracks branches correctly for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ternary/logical AND returning strings (not JSX):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please log in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// ✓ V8 tracks this&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error occurred&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// ✓ V8 tracks this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If/else statements:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginPrompt&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// ✓ V8 tracks this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ternary outside JSX expression container:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginPrompt&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ✓ V8 tracks this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Specific Blind Spots
&lt;/h3&gt;

&lt;p&gt;The blind spot occurs only when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using a ternary &lt;code&gt;? :&lt;/code&gt; or logical AND &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Inside a JSX expression container &lt;code&gt;{}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Returning JSX elements (not strings or primitives)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✗ Blind spots - V8 cannot track branch coverage&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComponentA&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComponentB&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComponentA&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Works - returning strings&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Works - using if/else&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComponentA&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComponentB&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Detecting Blind Spots
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using nextcov check
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;nextcov check&lt;/code&gt; command scans your codebase for these patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nextcov check src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V8 Coverage Blind Spots Found:
────────────────────────────────────────────────────────────

src/components/UserStatus.tsx:5:7
  ⚠ JSX ternary operator (V8 cannot track branch coverage)

src/components/ErrorDisplay.tsx:4:7
  ⚠ JSX logical AND (V8 cannot track branch coverage)

────────────────────────────────────────────────────────────
Found 2 issues in 2 files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using ESLint
&lt;/h3&gt;

&lt;p&gt;For real-time detection during development, use &lt;a href="https://www.npmjs.com/package/eslint-plugin-v8-coverage" rel="noopener noreferrer"&gt;eslint-plugin-v8-coverage&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; eslint-plugin-v8-coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;v8Coverage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eslint-plugin-v8-coverage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;v8Coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommended&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flags JSX ternary and logical AND patterns as errors, reminding you to ensure both branches are tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workarounds
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Write Explicit Tests for Both Branches
&lt;/h3&gt;

&lt;p&gt;The simplest solution: ensure your tests cover both cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// UserStatus.test.tsx&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows welcome message when logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserStatus&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows login prompt when not logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserStatus&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please log in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though V8 won't track the branch coverage accurately, your tests will fail if either branch is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Extract Conditional to a Variable
&lt;/h3&gt;

&lt;p&gt;Move the conditional outside the JSX expression container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: V8 blind spot (0 branches reported)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;helperText&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-red-600"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;helperText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-500"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;helperText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: V8 tracks branches correctly (7 branches vs 4 before)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;helperText&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-red-600"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;helperElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;helperText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-500"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;helperText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errorElement&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;helperElement&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By extracting the ternary to a variable &lt;strong&gt;outside&lt;/strong&gt; the JSX &lt;code&gt;{}&lt;/code&gt;, V8 can properly track the branch. The variable reference &lt;code&gt;{errorElement}&lt;/code&gt; inside JSX is just a simple expression, not a conditional.&lt;/p&gt;

&lt;p&gt;In the example above, the "before" version reports only 4 branches total (none for lines with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;). After extracting to variables, V8 reports 7 branches and correctly tracks coverage for the conditionals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double AND pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: V8 blind spot (0 branches for this line)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminPanel&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: V8 tracks branches correctly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminPanel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminPanel&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;adminPanel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: convert &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chains that return JSX into explicit ternary expressions (&lt;code&gt;? : null&lt;/code&gt;) outside the JSX container.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Early Returns with if/else
&lt;/h3&gt;

&lt;p&gt;For simpler components, use if/else statements instead of JSX ternaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: V8 blind spot&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginPrompt&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: V8 tracks branches correctly&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginPrompt&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V8 correctly tracks &lt;code&gt;if/else&lt;/code&gt; statement branches, so coverage reports will be accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Accept the Limitation
&lt;/h3&gt;

&lt;p&gt;Sometimes the cleanest code uses JSX ternaries, and that's fine. Just be aware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;V8 coverage numbers for branch coverage may be inflated&lt;/li&gt;
&lt;li&gt;Write tests that explicitly cover both branches&lt;/li&gt;
&lt;li&gt;Use the ESLint plugin or &lt;code&gt;nextcov check&lt;/code&gt; to identify these patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Other V8 Limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Source Map Dependency
&lt;/h3&gt;

&lt;p&gt;V8 coverage reports byte ranges in bundled code. Without source maps, you can't map back to original files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;productionBrowserSourceMaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;E2E_MODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;E2E_MODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devtool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bundler Transformations
&lt;/h3&gt;

&lt;p&gt;Code transformations can affect coverage accuracy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tree shaking&lt;/strong&gt; removes unused code — can't measure coverage for code that doesn't exist in the bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code splitting&lt;/strong&gt; may load chunks conditionally — coverage depends on which chunks are loaded during tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minification&lt;/strong&gt; without source maps makes coverage meaningless&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Multi-Process Coordination
&lt;/h3&gt;

&lt;p&gt;V8 coverage is collected per-process. For Next.js applications, you need to coordinate coverage from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js server process (Server Components, Server Actions)&lt;/li&gt;
&lt;li&gt;Browser process (Client Components)&lt;/li&gt;
&lt;li&gt;Test runner process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; handle this coordination automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Workaround&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ternary/logical AND returning JSX inside &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Branch coverage not tracked&lt;/td&gt;
&lt;td&gt;Write explicit tests for both branches, use ESLint plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source map dependency&lt;/td&gt;
&lt;td&gt;Coverage on bundled code only&lt;/td&gt;
&lt;td&gt;Enable source maps for test builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundler transformations&lt;/td&gt;
&lt;td&gt;Some code may be excluded&lt;/td&gt;
&lt;td&gt;Understand your bundler's behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-process coordination&lt;/td&gt;
&lt;td&gt;Coverage incomplete&lt;/td&gt;
&lt;td&gt;Use tools like nextcov&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;V8 coverage is the practical choice for modern Next.js applications, but understanding its limitations helps you write more comprehensive tests and interpret coverage reports accurately.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; — E2E coverage for Next.js with Playwright&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/eslint-plugin-v8-coverage" rel="noopener noreferrer"&gt;eslint-plugin-v8-coverage&lt;/a&gt; — Detect V8 coverage blind spots&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/javascript-code-coverage" rel="noopener noreferrer"&gt;V8 Blog: JavaScript Code Coverage&lt;/a&gt; — How V8 coverage works at the engine level&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part 4 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;nextcov - Collecting Test Coverage for Next.js Server Components&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip"&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8"&gt;V8 Coverage vs Istanbul: Performance and Accuracy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;V8 Coverage Limitations and How to Work Around Them (this article)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f"&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf"&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Update History
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;January 2025&lt;/strong&gt;: The V8 coverage limitation described in this article has been fixed in &lt;code&gt;ast-v8-to-istanbul@0.3.10&lt;/code&gt;. The &lt;code&gt;nextcov check&lt;/code&gt; command no longer scans for these patterns, and &lt;code&gt;eslint-plugin-v8-coverage&lt;/code&gt; has been deprecated.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>testing</category>
      <category>react</category>
      <category>playwright</category>
    </item>
    <item>
      <title>V8 Coverage vs Istanbul: Performance and Accuracy</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 17:09:26 +0000</pubDate>
      <link>https://forem.com/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8</link>
      <guid>https://forem.com/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8</guid>
      <description>&lt;p&gt;When it comes to JavaScript code coverage, Istanbul has been the industry standard for over a decade. But V8 native coverage is gaining traction, especially for modern frameworks. How do they compare in terms of performance and accuracy?&lt;/p&gt;

&lt;h2&gt;
  
  
  How They Work: Fundamentally Different Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Istanbul: Instrumentation-Based
&lt;/h3&gt;

&lt;p&gt;Istanbul works by &lt;strong&gt;transforming your source code&lt;/strong&gt; before execution. It injects counter variables that track which lines, branches, and functions are executed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original code&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, stranger!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After Istanbul instrumentation&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Function counter&lt;/span&gt;
  &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Statement counter&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Branch counter (true)&lt;/span&gt;
    &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Branch counter (false)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;cov_abc123&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, stranger!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This transformation happens at build time via Babel (&lt;code&gt;babel-plugin-istanbul&lt;/code&gt;) or other AST transformers.&lt;/p&gt;

&lt;h3&gt;
  
  
  V8: Runtime Coverage
&lt;/h3&gt;

&lt;p&gt;V8 coverage works at the &lt;strong&gt;JavaScript engine level&lt;/strong&gt;. V8 (the engine powering Node.js and Chrome) has built-in support for tracking code execution — no source code transformation needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable V8 coverage in Node.js&lt;/span&gt;
&lt;span class="nv"&gt;NODE_V8_COVERAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./coverage node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V8 tracks execution by recording which byte ranges of each script were executed. The output is a list of ranges with execution counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scriptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ranges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endOffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Istanbul:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires a separate instrumentation pass during build&lt;/li&gt;
&lt;li&gt;Must transform every file, even those not tested&lt;/li&gt;
&lt;li&gt;Adds overhead to build time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;V8:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero build time overhead&lt;/li&gt;
&lt;li&gt;No transformation needed&lt;/li&gt;
&lt;li&gt;Code runs exactly as in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Runtime Performance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Istanbul:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instrumented code runs slower due to counter increments&lt;/li&gt;
&lt;li&gt;Additional memory usage from counter objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;V8:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal runtime overhead (coverage is optimized at the engine level)&lt;/li&gt;
&lt;li&gt;Near-production performance during tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accuracy Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Istanbul is always more accurate&lt;/strong&gt; because it instruments your original source code before any transformation. Every line, branch, and statement is tracked exactly as you wrote it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V8 coverage&lt;/strong&gt; works on bundled/transformed code and relies on source maps to map back to your original files. This introduces potential inaccuracies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source map mappings aren't always perfect&lt;/li&gt;
&lt;li&gt;Bundler transformations can merge or split code&lt;/li&gt;
&lt;li&gt;Some patterns become untrackable after transformation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;V8 also has specific blind spots with JSX patterns like ternary operators and logical AND expressions. We'll cover these in detail in the next article: &lt;em&gt;V8 Coverage Limitations and How to Work Around Them&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use Istanbul When:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You need precise branch coverage&lt;/strong&gt; — Every &lt;code&gt;if/else&lt;/code&gt;, ternary, and logical operator is tracked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSX branch coverage matters&lt;/strong&gt; — Istanbul can track JSX conditional rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're using Babel anyway&lt;/strong&gt; — Adding istanbul plugin is trivial&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests only&lt;/strong&gt; — No multi-process complexity&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Use V8 When:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Performance is critical&lt;/strong&gt; — Near-zero overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing bundled code&lt;/strong&gt; — Works on any output (SWC, esbuild, webpack)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E/integration tests&lt;/strong&gt; — Can collect coverage across processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern tooling&lt;/strong&gt; — Works with SWC, which Istanbul doesn't support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js App Router&lt;/strong&gt; — The only option that works with Server Actions&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  You Can't Mix V8 and Istanbul Coverage
&lt;/h2&gt;

&lt;p&gt;A common misconception: since tools like &lt;code&gt;@vitest/coverage-v8&lt;/code&gt; and &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; output Istanbul-format reports, you might think you can mix V8-based and Istanbul instrumentation-based coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even though the output format is the same (Istanbul JSON), the underlying data is fundamentally different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Istanbul instrumentation&lt;/strong&gt; tracks counters injected into your original source code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V8 coverage&lt;/strong&gt; tracks byte ranges in bundled/transformed code, mapped back via source maps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you try to merge them, the line/branch mappings don't align. The same line of code may have different coverage data structures depending on how it was collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; Pick one approach and stick with it across all your test types.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you use &lt;code&gt;@vitest/coverage-v8&lt;/code&gt; for unit tests, use &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; for E2E tests — they can merge&lt;/li&gt;
&lt;li&gt;If you use &lt;code&gt;@vitest/coverage-istanbul&lt;/code&gt; for unit tests, you cannot merge with nextcov's E2E coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Neither Istanbul nor V8 coverage is universally better — they have different strengths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Istanbul&lt;/th&gt;
&lt;th&gt;V8&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build overhead&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime overhead&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch accuracy&lt;/td&gt;
&lt;td&gt;Better&lt;/td&gt;
&lt;td&gt;Good (with blind spots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundler compatibility&lt;/td&gt;
&lt;td&gt;Babel only&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-process&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For modern Next.js applications, V8 coverage is often the only practical choice because Istanbul doesn't work with SWC and Server Actions. Use &lt;code&gt;@vitest/coverage-v8&lt;/code&gt; for unit tests and &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; for E2E tests — they can be merged into a unified report.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; — Collect V8 coverage from both server and browser during Playwright E2E tests for Next.js applications&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@vitest/coverage-v8" rel="noopener noreferrer"&gt;@vitest/coverage-v8&lt;/a&gt; — V8 coverage provider for Vitest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jestjs/jest/issues/11188" rel="noopener noreferrer"&gt;Jest Issue #11188: V8 Coverage Tradeoffs&lt;/a&gt; — Documents V8 coverage limitations (implicit else, block-level tracking)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part 3 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;nextcov - Collecting Test Coverage for Next.js Server Components&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip"&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;V8 Coverage vs Istanbul: Performance and Accuracy (this article)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2"&gt;V8 Coverage Limitations and How to Work Around Them&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f"&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf"&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>playwright</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why Istanbul Coverage Doesn't Work with Next.js App Router</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 04:06:46 +0000</pubDate>
      <link>https://forem.com/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip</link>
      <guid>https://forem.com/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip</guid>
      <description>&lt;p&gt;If you've tried to set up code coverage for a Next.js 13+ application with the App Router, you've likely hit a wall. The traditional approach — using Istanbul with &lt;code&gt;babel-plugin-istanbul&lt;/code&gt; — simply doesn't work anymore. Here's why, and what you can do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Traditional Approach: Istanbul + Babel
&lt;/h2&gt;

&lt;p&gt;For years, Istanbul has been the standard for JavaScript code coverage. It works by &lt;strong&gt;instrumenting your code at build time&lt;/strong&gt; — injecting counter variables that track which lines, branches, and functions are executed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original code&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After Istanbul instrumentation&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cov_1234&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Function counter&lt;/span&gt;
  &lt;span class="nx"&gt;cov_1234&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Statement counter&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach requires a Babel plugin (&lt;code&gt;babel-plugin-istanbul&lt;/code&gt;) to transform your code during the build process. It worked great with Create React App, older Next.js versions, and any project using Babel as its compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Next.js Moved to SWC
&lt;/h2&gt;

&lt;p&gt;Starting with Next.js 12, and becoming the default in Next.js 13+, the framework switched from Babel to &lt;strong&gt;SWC&lt;/strong&gt; (Speedy Web Compiler) for transpilation. SWC is written in Rust and is significantly faster than Babel.&lt;/p&gt;

&lt;p&gt;But here's the catch: &lt;strong&gt;Istanbul's instrumentation plugin only works with Babel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you try to add a &lt;code&gt;.babelrc&lt;/code&gt; file to enable Istanbul, Next.js detects it and falls back to Babel — but this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disables SWC's performance benefits&lt;/li&gt;
&lt;li&gt;Can break other SWC-dependent features&lt;/li&gt;
&lt;li&gt;Often causes configuration conflicts with Next.js's internal setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The App Router Makes It Worse
&lt;/h2&gt;

&lt;p&gt;Next.js 13 introduced the App Router with React Server Components (RSC). You might think: "I'll just add a &lt;code&gt;.babelrc&lt;/code&gt; and force Babel mode." But the App Router creates a critical problem:&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Actions Break with Babel
&lt;/h3&gt;

&lt;p&gt;When you enable Babel in a Next.js project that uses Server Actions, &lt;strong&gt;the build fails&lt;/strong&gt;. Server Actions require special compiler handling that SWC provides but Babel doesn't support correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to build this with a &lt;code&gt;.babelrc&lt;/code&gt; file present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm run build
&lt;span class="c"&gt;# Build fails with Server Action compilation errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a configuration issue — it's a fundamental incompatibility. Next.js's Server Action transformation relies on SWC-specific features that &lt;code&gt;babel-plugin-istanbul&lt;/code&gt; can't work with.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Catch-22
&lt;/h3&gt;

&lt;p&gt;You're stuck in an impossible situation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Istanbul requires Babel&lt;/strong&gt; → Add &lt;code&gt;.babelrc&lt;/code&gt; to enable instrumentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Actions require SWC&lt;/strong&gt; → Build fails with Babel enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No coverage for Server Actions&lt;/strong&gt; → Can't measure what your E2E tests exercise&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Common Workaround: Skip E2E Coverage Entirely
&lt;/h2&gt;

&lt;p&gt;Many teams simply give up on E2E coverage and rely only on unit tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unit tests (Vitest/Jest) → Istanbul coverage ✓
E2E tests (Playwright/Cypress) → No coverage ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server Components remain completely untested for coverage&lt;/li&gt;
&lt;li&gt;No visibility into what E2E tests actually exercise&lt;/li&gt;
&lt;li&gt;Coverage reports show artificially low numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: V8 Native Coverage
&lt;/h2&gt;

&lt;p&gt;The fundamental issue is that Istanbul requires &lt;strong&gt;pre-instrumentation&lt;/strong&gt; of source code. But there's another approach: &lt;strong&gt;runtime coverage collection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;V8, the JavaScript engine that powers Node.js and Chrome, has built-in coverage support. It tracks code execution at the engine level — no instrumentation needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How V8 Coverage Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;For Node.js (server):&lt;/strong&gt; Set &lt;code&gt;NODE_V8_COVERAGE=./coverage&lt;/code&gt; environment variable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Chrome (browser):&lt;/strong&gt; Use Chrome DevTools Protocol (CDP) to enable coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After tests:&lt;/strong&gt; Read the coverage data from V8's native format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach works with any bundler, any compiler (Babel, SWC, esbuild), and any framework. The code runs exactly as it would in production — V8 just tracks what gets executed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Critical Role of Source Maps
&lt;/h3&gt;

&lt;p&gt;There's one catch: V8 coverage reports line/column positions in the &lt;strong&gt;bundled code&lt;/strong&gt;, not your original TypeScript/JSX files. To get meaningful coverage reports, you need &lt;strong&gt;source maps&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;productionBrowserSourceMaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;E2E_MODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isServer&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;E2E_MODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devtool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build with the environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;E2E_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With source maps, the coverage tool can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Map bundled code positions back to original source files&lt;/li&gt;
&lt;li&gt;Show coverage for your TypeScript/JSX, not minified JavaScript&lt;/li&gt;
&lt;li&gt;Produce reports that reference your actual codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why &lt;code&gt;nextcov&lt;/code&gt; requires source maps in your production build — without them, you'd only see coverage for unreadable bundled code.&lt;/p&gt;

&lt;h3&gt;
  
  
  V8 Coverage with Next.js
&lt;/h3&gt;

&lt;p&gt;For Next.js applications, you need to collect coverage from multiple processes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Coverage Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js Server (RSC, Server Actions)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NODE_V8_COVERAGE&lt;/code&gt; env var&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser (Client Components)&lt;/td&gt;
&lt;td&gt;Playwright CDP integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Runner&lt;/td&gt;
&lt;td&gt;Orchestrates collection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is exactly what &lt;a href="https://github.com/stevez/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt; does. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collects V8 coverage from both server and browser during Playwright E2E tests&lt;/li&gt;
&lt;li&gt;Uses source maps to map bundled code back to original TypeScript/JSX&lt;/li&gt;
&lt;li&gt;Converts V8 format to Istanbul format for familiar reports&lt;/li&gt;
&lt;li&gt;Merges with unit test coverage for a complete picture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started with V8 Coverage
&lt;/h2&gt;

&lt;p&gt;If you're ready to move beyond Istanbul for your Next.js App Router application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nextcov &lt;span class="nt"&gt;--save-dev&lt;/span&gt;
npx nextcov init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; command sets up everything you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global setup/teardown for Playwright&lt;/li&gt;
&lt;li&gt;CDP connection to Next.js server&lt;/li&gt;
&lt;li&gt;Source map processing&lt;/li&gt;
&lt;li&gt;Report generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run your E2E tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build with source maps&lt;/span&gt;
&lt;span class="nv"&gt;E2E_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Start server with coverage and run tests&lt;/span&gt;
&lt;span class="nv"&gt;NODE_V8_COVERAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.v8-coverage &lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect=9230'&lt;/span&gt; npm start &amp;amp;
npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Istanbul served us well for over a decade, but the JavaScript ecosystem has evolved. With:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SWC&lt;/strong&gt; replacing Babel as the default compiler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Actions&lt;/strong&gt; requiring SWC-specific transformations that break Babel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...it's time for a new approach.&lt;/p&gt;

&lt;p&gt;V8 native coverage works with modern tooling, not against it. It collects accurate coverage data from both client and server without requiring any code transformation.&lt;/p&gt;

&lt;p&gt;If you're building with Next.js App Router and want complete coverage visibility, check out &lt;a href="https://github.com/stevez/nextcov" rel="noopener noreferrer"&gt;nextcov&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 2 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc"&gt;nextcov - Collecting Test Coverage for Next.js Server Components&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router (this article)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8"&gt;V8 Coverage vs Istanbul: Performance and Accuracy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2"&gt;V8 Coverage Limitations and How to Work Around Them&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f"&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf"&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>testing</category>
      <category>playwright</category>
    </item>
    <item>
      <title>nextcov - Collecting Test Coverage for Next.js Server Components</title>
      <dc:creator>Steve Zhang</dc:creator>
      <pubDate>Thu, 01 Jan 2026 01:51:51 +0000</pubDate>
      <link>https://forem.com/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc</link>
      <guid>https://forem.com/stevez/nextcov-collecting-test-coverage-for-nextjs-server-components-6gc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📝 UPDATE (January 2025):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As of &lt;code&gt;nextcov@1.1.0&lt;/code&gt; with &lt;code&gt;ast-v8-to-istanbul@0.3.10&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSX branch coverage now works!&lt;/strong&gt; Ternary (&lt;code&gt;? :&lt;/code&gt;) and logical AND (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;) patterns in JSX now have proper branch coverage tracking&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;nextcov check&lt;/code&gt; command now only checks project configuration (not JSX patterns)&lt;/li&gt;
&lt;li&gt;These were not V8 limitations but bugs in the V8-to-Istanbul conversion that are now fixed&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've ever tried to get test coverage for React Server Components, you know the frustration. Unit testing frameworks like Jest and Vitest can't render server components — they require the full Next.js runtime. So you write E2E tests with Playwright, but then you're left wondering: "What's actually being covered?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nextcov&lt;/strong&gt; solves this problem by collecting V8 coverage from both client and server during Playwright E2E tests, then merging it with your unit and component test coverage for a complete picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Landscape in 2025
&lt;/h2&gt;

&lt;p&gt;Modern Next.js applications have three layers that need testing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What to Test&lt;/th&gt;
&lt;th&gt;Best Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utilities &amp;amp; Hooks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pure functions, custom hooks&lt;/td&gt;
&lt;td&gt;Vitest (unit tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client Components&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React components with user interaction&lt;/td&gt;
&lt;td&gt;Vitest Browser Mode (component tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server Components&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RSC, server actions, full pages&lt;/td&gt;
&lt;td&gt;Playwright (E2E tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Vitest Browser Mode — Great for Client Components
&lt;/h3&gt;

&lt;p&gt;Vitest's &lt;a href="https://vitest.dev/guide/browser/" rel="noopener noreferrer"&gt;Browser Mode&lt;/a&gt; is excellent for component testing. It runs tests in a real browser, giving you accurate DOM behavior and event handling. Combined with &lt;code&gt;@vitest/coverage-v8&lt;/code&gt;, you get proper coverage for your client components.&lt;/p&gt;

&lt;p&gt;But there's a limitation: &lt;strong&gt;Vitest Browser Mode only works for client components&lt;/strong&gt;. Server components require the full Next.js runtime — they can't be rendered in isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Server Component Gap
&lt;/h3&gt;

&lt;p&gt;React Server Components (RSC) are notoriously difficult to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They run only on the server&lt;/strong&gt; — Can't be rendered in jsdom or even a real browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async components fetch data directly&lt;/strong&gt; — Mocking becomes complex and unreliable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tight coupling with Next.js runtime&lt;/strong&gt; — Server actions, cookies, headers require the full framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical solution? Test server components through E2E tests with Playwright, where they run in their natural environment.&lt;/p&gt;

&lt;p&gt;But this creates a coverage gap. You have coverage from unit tests, coverage from component tests... but nothing from E2E tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  nextcov Fills the Gap
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;nextcov&lt;/strong&gt; completes the coverage picture by collecting V8 coverage during Playwright E2E tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Type&lt;/th&gt;
&lt;th&gt;Coverage Tool&lt;/th&gt;
&lt;th&gt;What It Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit (Vitest)&lt;/td&gt;
&lt;td&gt;@vitest/coverage-v8&lt;/td&gt;
&lt;td&gt;Utilities, hooks, logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component (Vitest Browser)&lt;/td&gt;
&lt;td&gt;@vitest/coverage-v8&lt;/td&gt;
&lt;td&gt;Client components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2E (Playwright)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;nextcov&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server components, pages, user flows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the best part? &lt;strong&gt;nextcov merges all three into a single unified report.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Results
&lt;/h2&gt;

&lt;p&gt;In a production Next.js application:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coverage Type&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit Tests&lt;/td&gt;
&lt;td&gt;~45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component Tests&lt;/td&gt;
&lt;td&gt;~35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2E Tests (nextcov)&lt;/td&gt;
&lt;td&gt;~46%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~88%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each test type contributes coverage that others can't reach. The merged report shows exactly what's tested and what's not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Unlike traditional coverage tools that work within a single Node.js process, nextcov coordinates coverage collection across three separate environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test Runner&lt;/strong&gt; (Playwright) — orchestrates tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js Server&lt;/strong&gt; (Node.js) — runs server components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser&lt;/strong&gt; (Chromium) — runs client components&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the browser, nextcov uses Playwright's CDP integration to collect V8 coverage. For the server, it uses Node.js's built-in &lt;code&gt;NODE_V8_COVERAGE&lt;/code&gt; environment variable, triggered via Chrome DevTools Protocol.&lt;/p&gt;

&lt;p&gt;This multi-process approach is what makes nextcov unique compared to tools like c8 or @vitest/coverage-v8, which only work within a single process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;nextcov &lt;span class="nt"&gt;--save-dev&lt;/span&gt;

&lt;span class="c"&gt;# Interactive setup&lt;/span&gt;
npx nextcov init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; command creates all the necessary configuration files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;e2e/global-setup.ts&lt;/code&gt; — Initialize coverage collection&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e2e/global-teardown.ts&lt;/code&gt; — Finalize and generate reports&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e2e/fixtures/test-fixtures.ts&lt;/code&gt; — Playwright fixture for per-test coverage&lt;/li&gt;
&lt;li&gt;Updates to &lt;code&gt;playwright.config.ts&lt;/code&gt; and &lt;code&gt;next.config.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run your E2E tests with coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build with source maps&lt;/span&gt;
&lt;span class="nv"&gt;E2E_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Start server with coverage enabled and run tests&lt;/span&gt;
&lt;span class="nv"&gt;NODE_V8_COVERAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.v8-coverage &lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect=9230'&lt;/span&gt; npm start &amp;amp;
npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Merging All Coverage
&lt;/h2&gt;

&lt;p&gt;The real power comes from merging E2E coverage with your unit and component test coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Merge all coverage sources&lt;/span&gt;
npx nextcov merge coverage/unit coverage/component coverage/e2e &lt;span class="nt"&gt;-o&lt;/span&gt; coverage/merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a unified HTML report showing the complete coverage picture — what's covered by unit tests, component tests, E2E tests, and what's still missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Project Configuration
&lt;/h2&gt;

&lt;p&gt;nextcov includes a &lt;code&gt;check&lt;/code&gt; command to validate your project configuration for common issues that affect V8 coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nextcov check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check command detects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing or outdated browserslist&lt;/strong&gt; — Can cause phantom branches from transpiled &lt;code&gt;?.&lt;/code&gt; and &lt;code&gt;??&lt;/code&gt; operators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Babel configuration&lt;/strong&gt; — May transpile modern syntax and break V8 coverage mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source maps not enabled&lt;/strong&gt; — Required for mapping bundled code back to source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing dependencies&lt;/strong&gt; — Playwright or Vitest not installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project Configuration:
────────────────────────────────────────────────────────────
  ⚠ Missing browserslist - ?? and ?. operators may be transpiled, causing phantom branches
  ⚠ Source maps not enabled in next.config - add productionBrowserSourceMaps: true
    next.config.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Prior to v1.1.0, the &lt;code&gt;check&lt;/code&gt; command also scanned for JSX patterns (ternary and logical AND) that V8 couldn't track. As of v1.1.0 with &lt;code&gt;ast-v8-to-istanbul@0.3.10&lt;/code&gt;, these patterns now have proper branch coverage, so JSX scanning has been removed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Complete Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Here's the recommended approach for Next.js applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests (Vitest)&lt;/strong&gt; — Test utilities, hooks, and pure logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component tests (Vitest Browser Mode)&lt;/strong&gt; — Test client components in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E tests (Playwright + nextcov)&lt;/strong&gt; — Test server components, pages, and user flows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge coverage&lt;/strong&gt; — Get the complete picture with &lt;code&gt;nextcov merge&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you fast feedback from unit and component tests during development, comprehensive E2E tests for critical user flows, and a unified coverage report that shows exactly what's tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + Vite support&lt;/strong&gt; — Works with both frameworks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client + Server coverage&lt;/strong&gt; — Browser and Node.js in one report&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev mode support&lt;/strong&gt; — Works with &lt;code&gt;next dev&lt;/code&gt; (no production build required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detection&lt;/strong&gt; — Automatically detects dev vs production mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source map support&lt;/strong&gt; — Maps bundled code back to original TypeScript/JSX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-source merge&lt;/strong&gt; — Combine unit, component, and E2E coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config validation&lt;/strong&gt; — Check project configuration with &lt;code&gt;nextcov check&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple reporters&lt;/strong&gt; — HTML, LCOV, JSON, text-summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/stevez/nextcov" rel="noopener noreferrer"&gt;https://github.com/stevez/nextcov&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/nextcov" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/nextcov&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example Project&lt;/strong&gt;: &lt;a href="https://github.com/stevez/nextcov-example" rel="noopener noreferrer"&gt;https://github.com/stevez/nextcov-example&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a Next.js application and want complete test coverage visibility across unit, component, and E2E tests, give nextcov a try.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 1 of a series on test coverage for modern React applications:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;nextcov - Collecting Test Coverage for Next.js Server Components (this article)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/why-istanbul-coverage-doesnt-work-with-nextjs-app-router-9ip"&gt;Why Istanbul Coverage Doesn't Work with Next.js App Router&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-vs-istanbul-performance-and-accuracy-3ei8"&gt;V8 Coverage vs Istanbul: Performance and Accuracy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/v8-coverage-limitations-and-how-to-work-around-them-2eh2"&gt;V8 Coverage Limitations and How to Work Around Them&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/how-to-merge-vitest-unit-component-and-e2e-test-coverage-438f"&gt;How to Merge Vitest Unit, Component, and E2E Test Coverage&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/stevez/e2e-coverage-in-nextjs-dev-mode-vs-production-mode-3lnf"&gt;E2E Coverage in Next.js: Dev Mode vs Production Mode&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>playwright</category>
      <category>react</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
