skip to content

With Vue Test Utils, what does `wrapper.emitted('submit')` return, and how do you assert an emitted event's payload and order?

level: juniorimportance: should knowfreq 55%

answer

  1. one entry per emit call
  2. each entry is an argument list
  3. undefined when never emitted
  4. root native events are recorded too

basics

~20 s

wrapper.emitted('submit') returns an array with one entry per emit call, each entry being that call's argument array, or undefined if it was never emitted. Index into it for order and compare the inner array for the payload.

solid answer

~40 s

Vue Test Utils records every custom event a component emits. `wrapper.emitted()` returns an object keyed by event name; `wrapper.emitted('submit')` returns that event's list, **one entry per emit, in order**, and each entry is the **array of arguments** passed to `emit`. So `emit('submit', { name: 'Ada' })` once gives `[[{ name: 'Ada' }]]`, and a test asserts `expect(wrapper.emitted('submit')).toHaveLength(1)` and `expect(wrapper.emitted('submit')![0]).toEqual([{ name: 'Ada' }])`. If the event never fired the result is `undefined`, not an empty array. Recording happens at emit time, so if the component emits after awaiting a request, settle that first. Order **across** different event names is not recorded; for that, pass listener props such as `onSubmit` that push into one shared log.

code

ts · 13 lines
ts
import { flushPromises, mount } from '@vue/test-utils'
import ContactForm from './ContactForm.vue'

test('emits saved with the new contact after the API resolves', async () => {
  const save = vi.fn(() => Promise.resolve({ id: 7 }))
  const wrapper = mount(ContactForm, { props: { save } })

  await wrapper.find('input').setValue('Ada')
  await wrapper.find('form').trigger('submit')
  await flushPromises()
  expect(wrapper.emitted('saved')).toHaveLength(1)
  expect(wrapper.emitted('saved')![0]).toEqual([{ id: 7, name: 'Ada' }])
})

go deeper

for a junior

Recall the shape: a list of calls, each an array of arguments, and undefined when the event never fired. Know how to assert count and payload.

for a middle

Explain timing: emits are recorded synchronously, but an emit after an awaited request or inside a watcher appears only after the matching wait.

for a senior

Avoid false signals from native root events, assert cross-event order with listener props, and read child events from the child's own wrapper.

for a principal

Define component contracts as events plus payloads, and keep tests asserting those contracts so internal refactors do not break the suite.

## What gets recorded When `mount()` from `@vue/test-utils` creates the app, it hooks into the devtools hook Vue calls on every component emit, so that every `emit(...)` call from a component in the tree is stored per component. The mounted component's wrapper exposes that record through `emitted()`: - `wrapper.emitted()` returns an object: keys are event names, values are lists of calls; - `wrapper.emitted('submit')` returns just the list for `submit`; - each item in that list is an **array of the arguments** of one `emit` call. The Vue Test Utils docs show it with a component that emits `greet` twice: `wrapper.emitted()` equals `{ greet: [['hello'], ['goodbye']] }`. ## The shape, precisely | Component code | `wrapper.emitted('submit')` | |---|---| | never emits `submit` | `undefined` | | `emit('submit')` once | `[[]]` | | `emit('submit', { name: 'Ada' })` once | `[[{ name: 'Ada' }]]` | | `emit('submit', 1)` then `emit('submit', 2)` | `[[1], [2]]` | | `emit('move', 10, 20)` for `move` | `[[10, 20]]` | The double nesting trips people up. The outer array is **calls**; the inner array is **arguments**. A single-payload event is therefore asserted with `[0]` to pick the call and then compared with an array: `toEqual([{ name: 'Ada' }])`, or with `[0][0]` to compare the payload object itself. ## Asserting count, payload and order 1. **Count**: `expect(wrapper.emitted('submit')).toHaveLength(1)`. This also fails clearly if the result is `undefined`. 2. **Payload**: `expect(wrapper.emitted('submit')![0]).toEqual([{ name: 'Ada' }])`. 3. **Order within one event**: compare the whole list, `toEqual([[1], [2]])`, since entries are stored in emit order. 4. **Absence**: `expect(wrapper.emitted('submit')).toBeUndefined()`, not `toEqual([])`. ## Order across different events Each event name has its own list, so `emitted()` does not say whether `validate` came before `submit`. When cross-event order matters, pass listeners as props. In Vue 3, a prop named `onSubmit` on a component vnode is a listener for `submit`, so: ```ts const log: string[] = [] mount(SignupForm, { props: { onValidate: () => log.push('validate'), onSubmit: () => log.push('submit') } }) ``` The test then asserts `expect(log).toEqual(['validate', 'submit'])`. ## Timing: emitted is synchronous, the emitter may not be `emit` is recorded the moment it is called; reading `emitted()` needs no `await` of its own. What matters is **when the component calls `emit`**: - emitted directly in a click handler: available right after `trigger` dispatches, and awaiting it is still good practice; - emitted after `await api.save()`: not there until that promise chain has run, so `await flushPromises()` before reading it; - emitted from a watcher: the watcher callback runs during Vue's next flush, so await the trigger or `nextTick()` first. ## A trap: native events on the root element Vue Test Utils also records **native DOM events** fired on the component's root element, when the component has a single element root, under the event's name. So `wrapper.emitted('click')` can contain an entry for a component that never calls `emit('click')`: the entry is the native `MouseEvent` from a click on its root. If the component declares `click` in `emits`, the native one is ignored for that name. Assert on custom events with distinctive names, or check what the payload is, to avoid confusing the two. ## Typing the result in TypeScript `emitted` is generic. `wrapper.emitted()` is typed as a record of lists, and `wrapper.emitted<[Contact]>('saved')` types each entry as a one-element tuple, so `wrapper.emitted<[Contact]>('saved')![0][0].name` type-checks. The return type includes `undefined`, which is why tests use a non-null assertion or check the length first. The generic is only a claim by the test; it does not validate anything at runtime, so the payload assertion is still what proves the shape. ## What to assert, and what not - Assert on the **contract**: the event name, how many times and with what payload. - Do not assert on internal events of child components through the parent's wrapper; find the child with `findComponent` and read its own `emitted()`. - Prefer the emitted payload over spying on internal functions, because the emitted event is what the parent actually receives.

  • Why is `expect(wrapper.emitted('submit')).toEqual([])` the wrong way to assert that an event was not emitted?
    A never-emitted event returns `undefined`, not an empty array, so that assertion fails exactly when the component behaves correctly. Use `toBeUndefined()` to assert absence, and `toHaveLength(n)` to assert presence, which reports both a missing event and a wrong count clearly.
  • How do you read the events a child component emitted when mounting its parent?
    Find the child's wrapper with `wrapper.findComponent(ChildForm)` and call `emitted()` on that. Recording is per component instance, so the parent wrapper's `emitted()` shows only the parent's own events and native events on its root, not the child's custom events.

saying these in an interview costs you the question

  • emitted('submit') returns the payload object directly, not a list of calls.
  • An event that never fired shows up as an empty array.
  • emitted() records one global timeline of all events in order.
  • emitted only contains events the component emits itself, never native ones.
  • You must await nextTick before an emitted event becomes visible.