Skip to content

[Compiler Bug]: Coverage report shows missing branch coverage #32950

Open
@ValentinGurkov

Description

@ValentinGurkov

What kind of issue is this?

  • React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
  • babel-plugin-react-compiler (build issue installing or using the Babel plugin)
  • eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
  • react-compiler-healthcheck (build issue installing or using the healthcheck script)

Link to repro

https://github.com/ValentinGurkov/vitest-react-compiler-missing-coverage-repro

Repro steps

Describe the bug

Hello,

I've been testing out the new React compiler and have noticed that the code coverage report becomes incorrect with the compiler turned it. I believe it may be related to the way it changes the output react component code.

I've created a minimal reproduction repository to demonstrate the issue:
👉 https://github.com/ValentinGurkov/vitest-react-compiler-missing-coverage-repro, but I also want to share my findings here:

The button we are going to test is a simple one:

export const Button = () => {
    return <button>Click me</button>;
};

As well as its test:

import { page } from '@vitest/browser/context';
import { Button } from '@repo/components/ui/button.js';
import { describe, expect, it} from 'vitest';
import { render } from 'vitest-browser-react';

describe(Button, () => {
    it('renders with default variants', async () => {
        render(<Button />);

        const button = page.getByRole('button', { name: 'Click me' });

        await expect.element(button).toBeInTheDocument();
    });
});

The coverage report with the React compiler turned on looks like:

   ✓  browser (chromium)  test/components/ui/button.test.tsx (1 test) 9ms
   ✓ Button > renders with default variants 9ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  20:34:46
   Duration  632ms (transform 0ms, setup 133ms, collect 28ms, tests 9ms, environment 0ms, prepare 93ms)

 % Coverage report from v8
------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |       50 |     100 |     100 |                   
 button.tsx |     100 |       50 |     100 |     100 | 2                 
------------|---------|----------|---------|---------|-------------------

The component has no branching logic while the report shows are the are missing some. I believe I've managed to set up the vitest configuration to also log the transformed component and we have:

import { jsxDEV } from "react/jsx-dev-runtime";
import { c as _c } from "react/compiler-runtime";
export const Button = () => {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = /* @__PURE__ */ jsxDEV("button", { children: "Click me" }, void 0, false, {
      fileName: "/Users/<my-user>/Projects/vitest-react-compiler-coverage/src/components/ui/button.tsx",
      lineNumber: 6,
      columnNumber: 10
    }, this);
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
};

It looks like the React compiler introduces a conditional for memoization purposes. That may explain the coverage issue, though it raises the question: is it even possible to get 100% branch coverage for components compiled like this?

The test without the React compiler looks like:

- react({
- babel: {
- plugins: [
- ["babel-plugin-react-compiler", {}]
- ],
- },
- }),
+ react(),
   ✓  browser (chromium)  test/components/ui/button.test.tsx (1 test) 10ms
   ✓ Button > renders with default variants 10ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  20:36:42
   Duration  490ms (transform 0ms, setup 36ms, collect 6ms, tests 10ms, environment 0ms, prepare 92ms)

 % Coverage report from v8
------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |     100 |     100 |                   
 button.tsx |     100 |      100 |     100 |     100 |                   
------------|---------|----------|---------|---------|-------------------

The code coverage is 100% as expected. The output code also has no conditions:

import { jsxDEV } from "react/jsx-dev-runtime";
export const Button = () => {
  return /* @__PURE__ */ jsxDEV("button", { children: "Click me" }, void 0, false, {
    fileName: "/Users/<my-user>/Projects/vitest-react-compiler-coverage/src/components/ui/button.tsx",
    lineNumber: 2,
    columnNumber: 12
  }, this);
};

Coming from vitest-dev/vitest#7843 (comment), it seems that babel's auxiliaryCommentBefore also does not have any effect on this.

How do we see test coverage working once the React Compiler becomes standard?

How often does this bug happen?

Every time

What version of React are you using?

19.1.0

What version of React Compiler are you using?

19.1.0-rc.1

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions