V8 Coverage Limitations and How to Work Around Them

Published: (January 1, 2026 at 02:38 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

❗ The Blind Spot

V8 does not recognise branches that are inside a JSX expression container ({}) when the conditional returns JSX elements (as opposed to strings or primitives).

Ternary inside JSX

function UserStatus({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    {isLoggedIn ? <LoggedIn /> : <LoggedOut />}
  );
}

V8 reports 0 branches for the line containing the ternary.
Even if your test only renders with isLoggedIn={true}, V8 won’t warn that <LoggedOut /> was never rendered.

Logical AND inside JSX

function ErrorDisplay({ error }: { error?: string }) {
  return (
    {error && <ErrorMessage error={error} />}
  );
}

Same problem – V8 reports 0 branches. Passing error={null} will not trigger a warning that <ErrorMessage /> was never rendered.

✅ What V8 does track correctly

PatternExampleV8 status
Ternary / logical AND returning strings{isLoggedIn ? 'Welcome!' : 'Please log in'}
{error && 'Error occurred'}
✅ Tracks branches
if / else statementsif (isLoggedIn) { return <LoggedIn />; } else { return <LoggedOut />; }✅ Tracks branches
Ternary outside JSX containerreturn isLoggedIn ? <LoggedIn /> : <LoggedOut />;✅ Tracks branches

The blind spot occurs only when:

  • Using a ternary ? : or logical AND && inside a JSX expression container ({})
  • The conditional returns JSX elements (not strings or other primitives)

📍 Blind‑spot examples

// ✗ V8 cannot track branch coverage
{condition ? <A /> : <B />}
{condition && <A />}
// ✓ Works – returning strings
{condition ? 'yes' : 'no'}
{condition && 'yes'}
// ✓ Works – using if/else
if (condition) return <A />;
return <B />;

🛠 Detecting Blind Spots with nextcov

npx nextcov check src/

Example output

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

📏 Real‑time detection – eslint‑plugin‑v8‑coverage

npm install -D eslint-plugin-v8-coverage
// eslint.config.js
import v8Coverage from 'eslint-plugin-v8-coverage';

export default [
  v8Coverage.configs.recommended,
];

The plugin flags JSX ternary and logical‑AND patterns as errors, reminding you to test both branches.

✅ Simple solution: write tests for both branches

// UserStatus.test.tsx
it('shows welcome message when logged in', () => {
  render(<UserStatus isLoggedIn={true} />);
  expect(screen.getByText('Welcome!')).toBeInTheDocument();
});

it('shows login prompt when not logged in', () => {
  render(<UserStatus isLoggedIn={false} />);
  expect(screen.getByText('Please log in')).toBeInDocument();
});

Even though V8 won’t count the branches, the tests will fail if either branch is broken.

🔧 Refactor: move the conditional outside the JSX container

Before (blind spot)

function Input({ error, helperText }: { error?: string; helperText?: string }) {
  return (
    <>
      {error && <ErrorMessage>{error}</ErrorMessage>}
      {helperText && !error && <Helper>{helperText}</Helper>}
    </>
  );
}

Result: V8 reports 4 branches total (none for the && lines).

After (V8 tracks correctly)

function Input({ error, helperText }: { error?: string; helperText?: string }) {
  const errorElement = error ? (
    <ErrorMessage>{error}</ErrorMessage>
  ) : null;

  const helperElement = helperText && !error ? (
    <Helper>{helperText}</Helper>
  ) : null;

  return (
    <>
      {errorElement}
      {helperElement}
    </>
  );
}

Result: V8 reports 7 branches, correctly covering the conditionals.

🔁 Double‑AND pattern

Before (blind spot)

{user && user.isAdmin && <AdminPanel />}

After (V8 tracks)

const adminPanel = user && user.isAdmin ? <AdminPanel /> : null;

/* later in JSX */
{adminPanel}

Key insight: Convert && chains that return JSX into explicit ternary expressions (? : null) outside the JSX container.

🧩 Refactor with if / else instead of JSX ternary

Before (blind spot)

function UserStatus({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    {isLoggedIn ? <LoggedIn /> : <LoggedOut />}
  );
}

After (V8 tracks)

function UserStatus({ isLoggedIn }: { isLoggedIn: boolean }) {
  if (isLoggedIn) {
    return <LoggedIn />;
  }

  return <LoggedOut />;
}

if / else statements are fully understood by V8, so coverage reports become accurate.

📋 Recommendations

  • Write explicit tests for both branches of any JSX conditional, even if V8 can’t count them.
  • Run nextcov check (or the ESLint plugin) to surface blind‑spot patterns early.
  • Refactor:
    • Move ternaries / logical‑ANDs that return JSX outside the JSX container.
    • Prefer if / else statements for complex branching.
  • Be aware that V8 branch‑coverage numbers may be inflated when JSX ternaries are kept.
  • Enable source maps in production builds if you need accurate line‑level reporting:
// next.config.ts
const nextConfig = {
  productionBrowserSourceMaps: !!process.env.E2E_MODE,
  webpack: (config) => {
    if (process.env.E2E_MODE) {
      config.devtool = 'source-map';
    }
    return config;
  },
};
export default nextConfig;
  • Remember that tree‑shaking and other code‑transformations can affect coverage accuracy because V8 reports byte ranges in the bundled output.

Coverage for Code That Doesn’t Exist in the Bundle

Why it matters

  • Code‑splitting can load chunks conditionally – coverage depends on which chunks are loaded during tests.
  • Minification without source maps makes coverage meaningless.
  • V8 coverage is collected per‑process. For a Next.js app you must coordinate coverage from three places:
    1. Next.js server process (Server Components, Server Actions)
    2. Browser process (Client Components)
    3. Test‑runner process

Tip: Tools like nextcov handle this coordination automatically.

Limitations, Impact & Work‑arounds

LimitationImpactWork‑around
Ternary / logical AND returning JSX inside {}Branch coverage not trackedWrite explicit tests for both branches, or use an ESLint plugin that forces explicit if/else.
Source‑map dependencyCoverage only on bundled codeEnable source maps for test builds (e.g., next build --sourceMaps).
Bundler transformationsSome code may be excluded from the bundle (e.g., dead‑code elimination)Understand your bundler’s behavior and add /* @__PURE__ */ comments or /* webpackIgnore: true */ where needed.
Multi‑process coordinationCoverage reports are incomplete or fragmentedUse a coordination tool such as nextcov (or roll your own merging script).

Key Takeaways

  • V8 coverage is the practical choice for modern Next.js applications.
  • Knowing its limitations helps you:
    1. Write more comprehensive tests.
    2. Interpret coverage reports accurately.

Helpful Resources

  • nextcov – E2E coverage for Next.js with Playwright
  • eslint-plugin-v8-coverage – Detect V8 coverage blind spots
  • V8 Blog: JavaScript Code Coverage – How V8 coverage works at the engine level
  1. nextcov – Collecting Test Coverage for Next.js Server Components
  2. Why Istanbul Coverage Doesn’t Work with Next.js App Router
  3. V8 Coverage vs Istanbul: Performance and Accuracy
  4. V8 Coverage Limitations and How to Work Around Them (this article)
  5. How to Merge Vitest Unit and Component Test Coverage (coming soon)
  6. E2E Coverage in Next.js: Dev Mode vs Production Mode (coming soon)
Back to Blog

Related posts

Read more »

The Web Ecosystem Civil War

markdown !Javadhttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads...

React Coding Challenge : Card Flip Game

React Card Flip Game – Code tsx import './styles.css'; import React, { useState, useEffect } from 'react'; const values = 1, 2, 3, 4, 5; type Card = { id: numb...