vitest-dev/vitest · error · Error

Invalid element or locator

Error message

Invalid element or locator: ${elementOrLocator}. Expected an instance of HTMLElement, SVGElement or Locator, received ${getType(elementOrLocator)}

What it means

expect.element() (the entry point for element assertions like expect(el).toBeVisible()) validates its argument up front: it must be null, undefined, an HTMLElement, an SVGElement, or an object carrying the internal Locator symbol ($$vitest:locator). Anything else throws immediately with the received type name. This guards the polling wrapper from receiving a value it cannot resolve to a DOM node.

Solutions

  1. Pass only HTMLElement/SVGElement (or null) or a Vitest Locator produced by page.getByRole/getByText etc.
  2. Await async lookups before passing: const el = await locator.element().
  3. If integrating a third-party locator, convert it to a Vitest Locator via page.elementLocator on its underlying Element.

Example fix

// before
expect(document.querySelectorAll('.btn')).toBeVisible()  // NodeList
// after
expect(document.querySelector('.btn')).toBeVisible()
Defensive patterns

Strategy: type-guard

Validate before calling

const kLocator = Symbol.for('$$vitest:locator')
function isValidTarget(v: unknown): boolean {
  return v == null || v instanceof HTMLElement || v instanceof SVGElement || (typeof v === 'object' && v !== null && kLocator in v)
}
if (!isValidTarget(el)) throw new Error('invalid target')
expect(el).toBeVisible()

Type guard

function isElementOrLocator(v: unknown): v is HTMLElement | SVGElement | Locator {
  const kLocator = Symbol.for('$$vitest:locator')
  return v instanceof HTMLElement || v instanceof SVGElement || (typeof v === 'object' && v !== null && kLocator in v)
}

Prevention

When it happens

Trigger: Calling expect(myString).toBeVisible(); passing a number, plain object, NodeList, or a third-party Locator-like object that lacks the $$vitest:locator symbol; passing a Promise that was not awaited.

Common situations: Mixing Vitest Locators with Playwright/RTL locators; forgetting to await an async query; passing the result of a querySelector without null-checking when it actually returned a non-Element (e.g. due to a wrapper).

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/646f8d5bf7ce2ca4. Report an issue: GitHub.

Appendix: source

Thrown at packages/browser/src/client/tester/expect-element.ts:16

import type { Assertion, ExpectPollOptions } from 'vitest'
import type { Locator } from 'vitest/browser'
import type { BrowserTraceEntryStatus } from './trace'
import { chai, expect } from 'vitest'
import { getType } from 'vitest/internal/browser'
import { getBrowserState, getWorkerState, now } from '../utils'
import { ariaMatchers } from './aria'
import { matchers } from './expect'
import { processTimeoutOptions } from './tester-utils'
import { createBrowserTraceRangeId, recordBrowserTraceEntry } from './trace'

const kLocator = Symbol.for('$$vitest:locator')

function element<T extends HTMLElement | SVGElement | null | Locator>(elementOrLocator: T, options?: ExpectPollOptions): Assertion<Promise<void>, HTMLElement | SVGElement | null> {
  if (elementOrLocator != null && !(elementOrLocator instanceof HTMLElement) && !(elementOrLocator instanceof SVGElement) && !(kLocator in elementOrLocator)) {
    throw new Error(`Invalid element or locator: ${elementOrLocator}. Expected an instance of HTMLElement, SVGElement or Locator, received ${getType(elementOrLocator)}`)
  }

  const pollOptions = processTimeoutOptions(options)
  const deadline = pollOptions?.timeout ? now() + pollOptions.timeout : undefined
  const expectElement = expect.poll(async function element(this: object): Promise<HTMLElement | SVGElement | null> {
    if (elementOrLocator instanceof Element || elementOrLocator == null) {
      return elementOrLocator
    }

    const isNot = chai.util.flag(this, 'negate') as boolean
    const name = chai.util.flag(this, '_name') as string
    // special case for `toBeInTheDocument` matcher
    if (isNot && name === 'toBeInTheDocument') {
      return elementOrLocator.query()
    }
    if (name === 'toHaveLength') {
      // we know that `toHaveLength` requires multiple elements,
      // but types generally expect a single one

View on GitHub (pinned to 1fa9837ec2)