vitest-dev/vitest · error · TypeError

toHaveFormValues must be called on a form or a fieldset…

Error message

toHaveFormValues must be called on a form or a fieldset, instead got ${getTag(formElement)}

What it means

.toHaveFormValues() reads named form controls, which only a FORM or FIELDSET element owns. If the target element has any other tag, the matcher throws a TypeError with the actual tag name, because there is no form-control collection to enumerate. The check uses the document's own HTMLFormElement/HTMLFieldSetElement constructors to support iframes and jsdom.

Solutions

  1. Query the actual <form> or <fieldset> element (e.g. container.querySelector('form')).
  2. If the component root is a div, drill down to the nested form before asserting.
  3. For custom elements, assert on individual fields with toHaveValue instead.

Example fix

// before
expect(formWrapperEl).toHaveFormValues({ email: 'a@b.c' })
// after
expect(formWrapperEl.querySelector('form')!).toHaveFormValues({ email: 'a@b.c' })
Defensive patterns

Strategy: type-guard

Validate before calling

const win = el.ownerDocument.defaultView || window
if (!(el instanceof win.HTMLFormElement) && !(el instanceof win.HTMLFieldSetElement)) {
  // drill down to the nested <form>
  el = el.querySelector('form')!
}
expect(el).toHaveFormValues(expected)

Type guard

function isFormLike(el: Element): el is HTMLFormElement | HTMLFieldSetElement {
  const win = el.ownerDocument.defaultView || window
  return el instanceof win.HTMLFormElement || el instanceof win.HTMLFieldSetElement
}

Prevention

When it happens

Trigger: Calling expect(divEl).toHaveFormValues({...}); calling on a section/article that wraps a form but is not itself a form/fieldset; passing the wrong element resolved from a Locator.

Common situations: Targeting a styled wrapper instead of the real <form>; copy-pasting form assertions onto custom form components whose root tag is a div; refactors that changed the queried selector.

Related errors


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

Appendix: source

Thrown at packages/browser/src/client/tester/expect/toHaveFormValues.ts:31

 * copies or substantial portions of the Software.
 */

import type { MatcherResult, MatcherState } from 'vitest'
import type { Locator } from '../locators'
import { cssEscape } from 'ivya/utils'
import { arrayAsSetComparison, getElementFromUserInput, getSingleElementValue, getTag } from './utils'

export default function toHaveFormValues(
  this: MatcherState,
  actual: Element | Locator,
  expectedValues: Record<string, unknown>,
): MatcherResult {
  const formElement = getElementFromUserInput(actual, toHaveFormValues, this)

  const defaultView = formElement.ownerDocument.defaultView || window

  if (!(formElement instanceof defaultView.HTMLFieldSetElement) && !(formElement instanceof defaultView.HTMLFormElement)) {
    throw new TypeError(`toHaveFormValues must be called on a form or a fieldset, instead got ${getTag(formElement)}`)
  }

  if (!expectedValues || typeof expectedValues !== 'object') {
    throw new TypeError(
      `toHaveFormValues must be called with an object of expected form values. Got ${expectedValues}`,
    )
  }

  const formValues = getAllFormValues(formElement)
  return {
    pass: Object.entries(expectedValues).every(([name, expectedValue]) =>
      this.equals(formValues[name], expectedValue, [arrayAsSetComparison, ...this.customTesters]),
    ),
    message: () => {
      const to = this.isNot ? 'not to' : 'to'
      const matcher = `${this.isNot ? '.not' : ''}.toHaveFormValues`

      const commonKeyValues: Record<string, unknown> = {}

View on GitHub (pinned to 1fa9837ec2)