Skip to content

Intermittent TypeError: undefined is not an object (evaluating 'e.includes') from render() in webkit when multiple test files are present #57

@joeprivett

Description

@joeprivett

Summary

render() from vitest-browser-react emits an asynchronous unhandled promise rejection in webkit when a test file with several render() calls runs alongside other test files. All assertions pass — the rejection appears in vitest's Unhandled Errors summary and causes Errors: 1 / non-zero exit.

The rejection originates inside render() at the page.elementLocator(container) call (pure-C_qo4W4L.js:59:37), which ultimately calls selectorEngine.generateSelectorSimple(element) where some value being .includes-ed is undefined.

  • Reproduces ~40–60% of CI runs locally
  • Reproduces only in webkit
  • Reproduces only when the suspect test file is run alongside ≥1 other test file
  • Reproduces with a pure unit test as the second file (so it's not "parallel rendering" — just "any second file present")
  • Coverage instrumentation (--coverage) makes it noticeably more frequent

Versions

vitest-browser-react 2.2.0
vitest 4.1.2
@vitest/browser 4.1.2
@vitest/browser-playwright 4.1.2
playwright 1.58.0
React 18.3.x
Node 22.x
OS macOS (Darwin 25.4.0)
Browsers run chromium, firefox, webkit (only webkit triggers)

Stack trace

TypeError: undefined is not an object (evaluating 'e.includes')
 ❯ render node_modules/.pnpm/vitest-browser-react@2.2.0_.../vitest-browser-react/dist/pure-C_qo4W4L.js:59:37

Line 59 in that bundle is:

const locator = page.elementLocator(container);

page.elementLocator in the playwright provider calls selectorEngine.generateSelectorSimple(element) — the e.includes lives somewhere inside that selector-generation path. The Safari/JSC error wording "undefined is not an object" is webkit-specific (V8 says "Cannot read properties of undefined").

vitest blames the test running at the time of the rejection, but the rejection is asynchronous and the cited test isn't the cause — it's just whichever was active when the framework's promise rejected.

Minimal reproducer

Create two test files in a project configured with vitest browser mode + the playwright provider + a webkit project:

a.test.tsx

import { describe, expect, test } from 'vitest'
import { render, page, userEvent } from 'vitest-browser-react'

// Anything that mounts a few times and has a hover interaction.
function Probe() {
  return (
    <div style={{ width: 400, height: 200 }}>
      <div data-testid="hover-area" style={{ width: '100%', height: '100%' }}>hover me</div>
    </div>
  )
}

describe('probe', () => {
  // ~10–15 tests doing render + hover. Single test isn't enough; the
  // race is roughly proportional to the number of renders in the file.
  for (let i = 0; i < 12; i++) {
    test(`render and hover #${i}`, async () => {
      render(<Probe />)
      await userEvent.hover(page.getByTestId('hover-area'))
      await expect.element(page.getByTestId('hover-area')).toBeInTheDocument()
    })
  }
})

b.test.ts — anything, even pure unit tests:

import { test, expect } from 'vitest'

test('placeholder', () => {
  expect(1 + 1).toBe(2)
})

vitest.config.ts

import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: 'webkit' }],
    },
  },
})

Run vitest --run --browser.headless repeatedly. Roughly half the runs report:

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
TypeError: undefined is not an object (evaluating 'e.includes')
 ❯ render node_modules/.../vitest-browser-react/dist/pure-C_qo4W4L.js:59:37

 Test Files  N passed (N)
      Tests  M passed (M)
     Errors  1 error

All assertions pass; only the Errors: 1 line is the symptom.

What I tried on our side

  • Removed setAttribute mutations outside React's commit phase (an aria-describedby effect that directly mutated an SVG element). Moving the attribute onto the JSX as a React prop reduced the rate from ~60% to ~40% but didn't eliminate it. So at least one contributor is on the framework side; bypassing React's reconciler appears to widen the race window.
  • Test interaction with cursor state. Webkit doesn't reset cursor position between tests in browser mode, which surfaced a different unrelated bug for us. Fixing that didn't change the rejection rate.
  • Switching userEvent.hover to a raw dispatchEvent (didn't help).
  • Splitting test files (untried — would reduce per-file render count, possible mitigation).

Workaround we're using

A narrow unhandledrejection listener in setupTests.ts that matches only rejections with this exact message AND a stack containing vitest-browser-react, then event.preventDefault()s them. Real test failures still surface. Comment explicitly references this issue and contains the deletion criteria once it's fixed upstream.

Expected behaviour

render() should not emit asynchronous unhandled rejections when called normally — the implicit cleanup between tests should fully settle before the next test's render begins. If the issue is in selectorEngine.generateSelectorSimple receiving a stale/null element, it should fail with a descriptive error from render()'s await rather than escaping as an unhandled rejection.

Guesses (low-confidence, just to help triage)

  • The race feels timing-sensitive (coverage instrumentation makes it worse).
  • The e.includes location suggests selectorEngine.generateSelectorSimple is calling .includes on an element property — possibly element.tagName, element.className, element.id, or an attribute value — that's undefined because the element was detached/torn down by the previous test's cleanup before the new elementLocator call.
  • mountedContainers / mountedRootEntries accounting in pure-C_qo4W4L.js might be involved if cleanup awaits don't fully settle in webkit; multiple containers from prior tests may have inconsistent state when elementLocator is called on a new one.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions