skip to content

Async Updates & Events

Awaiting trigger and setValue, flushPromises for pending requests, fake timers and emitted() assertions. Interviewers ask why an assertion right after a state change still reads the old DOM.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

What do Vue Test Utils' `trigger()` and `setValue()` return, and why must a test await them before checking the DOM?

level: juniorimportance: must knowfreq 65%

answer

  1. Vue batches DOM patches
  2. the method's return type
  3. a promise for the next tick
  4. input plus change for text fields

basics

~10 s

Both return a promise from Vue's nextTick. Dispatching the event updates state synchronously, but Vue applies the resulting DOM patch asynchronously, so a test that does not await reads the old DOM.

solid answer

~40 s

In Vue Test Utils, `trigger(event)` dispatches a DOM event on the element and returns `nextTick()`; `setValue(value)` on an input writes `element.value`, triggers `input` and then `change`, and returns the same kind of promise. `setProps` on the mounted component also returns `nextTick()`. The handlers run **synchronously** during dispatch, so reactive state has already changed, but Vue does not patch the DOM on each change: it queues the component's update and flushes the queue asynchronously. Awaiting the returned promise lets that flush happen, so `wrapper.text()` or `find()` then reads the updated DOM. Without `await`, the assertion runs against the **stale** DOM and fails, or passes by accident if it only checks something that did not change.

code

vue · 12 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const name = ref('')
</script>

<template>
  <form>
    <input v-model="name" />
    <p class="preview">Hello, {{ name }}</p>
  </form>
</template>

go deeper

for a junior

Recall that trigger, setValue and setProps return a promise and that you await it before asserting on the DOM.

for a middle

Explain the stale window: handlers run synchronously and change state, while Vue batches the DOM patch and applies it asynchronously, which nextTick waits for.

for a senior

Spot false greens from unawaited wrapper calls, know the disabled-element and checkbox no-op cases, and enforce awaits with a floating-promise lint rule.

for a principal

Set suite conventions for interaction helpers so every wrapper method is awaited and extra waits are tied to named async sources rather than scattered sleeps.

## The symptom A test clicks a button that increments a count, then immediately checks the text, and sees the old number. The component works in the browser. Nothing is wrong with it: the test read the DOM before Vue updated it. ## Why the DOM lags behind the state When reactive state changes in Vue 3, the components that render it do not re-render on the spot. Vue **queues** each affected component's update and applies all queued updates together, **asynchronously**, after the current synchronous code finishes. That batching means ten state changes in one handler cause one DOM patch, not ten. For a test, the consequence is a short window where: - the event handler has run and state is new; - the DOM still shows what was rendered before; - `wrapper.html()`, `wrapper.text()` and `find()` read that old DOM. The promise returned by Vue's `nextTick()` resolves after the pending updates have been flushed, so awaiting it closes the window. How the queue itself works is a runtime topic; for tests, knowing that the patch is deferred and that `nextTick` waits for it is enough. ## What the Vue Test Utils methods return The methods that typically cause an update return that promise themselves, so the test does not have to import `nextTick`: | Method | What it does first | Returns | |---|---|---| | `trigger('click')` | dispatches the DOM event if the element is not disabled | `nextTick()` | | `setValue('Ada')` on `<input>` or `<textarea>` | sets `element.value`, triggers `input`, then `change` | the promise from the last `trigger` | | `setValue(true)` on a checkbox | sets `checked` and triggers `input` and `change`, unless it already had that value | that promise, or nothing if skipped | | `setValue` on a `<select>` | selects the option(s), triggers `input` and `change` | the promise from the last `trigger` | | `setProps({ ... })` on the mounted component | writes new props into the reactive props object | `nextTick()` | Two details are worth knowing: - `setValue` fires **both** `input` and `change`, so it works for `v-model` and for `v-model.lazy`, which listens to `change`. - `trigger` on an element with a `disabled` attribute, such as a disabled `<button>`, does **not** dispatch the event, which mirrors the browser. It still returns `nextTick()`, so an awaited trigger on a disabled button quietly does nothing. ## Writing the test ```ts const wrapper = mount(NameForm) await wrapper.find('input').setValue('Ada') expect(wrapper.find('.preview').text()).toBe('Hello, Ada') ``` The pattern is always the same: 1. Perform the interaction through a wrapper method. 2. `await` the promise it returns. 3. Assert on the DOM. If the state change comes from somewhere else, such as a test calling a function on `wrapper.vm` or changing a ref it holds, the test must `await nextTick()` imported from `vue` itself, since no wrapper method was involved. ## What awaiting does not cover Awaiting the returned promise waits for Vue's pending DOM update. It does not wait for work the handler started that Vue knows nothing about, such as a request to an API, another promise chain or a `setTimeout`. For those, Vue Test Utils provides `flushPromises()`, and timers need a fake clock. The rule of thumb: **`await` every wrapper method, and add a further wait only for the specific async source that is still pending.** ## Common mistakes - **Chaining without awaiting**, such as calling `setValue` and then `trigger('submit')` with only the last one awaited; the intermediate update usually flushes in time, but the test no longer states what it waits for. - **Asserting through `wrapper.vm`** to dodge the wait; state is updated synchronously, so the test passes while never checking what the user sees. - **Triggering on the wrong element**, for example `trigger('click')` on a disabled button or on a wrapper that matched nothing useful, then blaming timing. - **Setting `element.value` by hand** without an `input` event, which leaves `v-model` state unchanged. ## Why a missing `await` sometimes passes An unawaited trigger does not always produce a red test. If the assertion checks something that was already true before the click, or reads state through `wrapper.vm` rather than the DOM, it passes regardless. That is why a linter rule for floating promises in tests is a cheap safeguard: it flags every unawaited wrapper method before it turns into a false green.

  • What does `setValue` do when called on a component wrapper rather than an input element?
    On a `VueWrapper`, `setValue(value)` emits `update:modelValue` from that component (or `update:<prop>` if you pass a prop name) and returns `nextTick()`. It simulates the child reporting a new model value to its parent, which is how you test a parent's `v-model` on a custom component without driving the child's internals.
  • Why does Vue Test Utils' `trigger` still work when a test has faked the clock?
    Vue's event listeners skip an event stamped at or before the moment the listener was attached, and a frozen fake clock can make those stamps equal. Vue Test Utils sets the event's internal timestamp to `Date.now() + 1` before dispatching, so the handler always fires. The update flush is promise-based, so the returned `nextTick()` also resolves under fake timers.

Vue's DOM updates work like a batched mail run: every state change drops a letter in the box, and the DOM changes only when the collection comes. Awaiting trigger is waiting for that collection before checking the mail.

saying these in an interview costs you the question

  • trigger updates the DOM synchronously, so awaiting it is only a style preference.
  • setValue sets the value but does not fire any input or change events.
  • Awaiting trigger also waits for API calls the click handler started.
  • A disabled button still receives the click when you call trigger on it.
  • The event handler itself runs on the next tick, not during dispatch.
open as a page

With Vue Test Utils, why is awaiting `trigger('submit')` not enough to see a form's success message after an API call, and what does `flushPromises()` add?

level: middleimportance: must knowfreq 55%

basics

~20 s

Awaiting trigger only waits for Vue's pending DOM update, not for the request promise the submit handler awaits. flushPromises() resolves on the next macrotask, so already-settled mock promises, their continuations and Vue's re-render all run first.

open as a page

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%

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.

open as a page

A Vue Test Utils test for a submit form passes locally but intermittently fails in CI, sometimes seeing 'Saving…' instead of 'Saved!'; which waiting mistakes cause that, and how do you make it deterministic?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The test is racing real time: an unawaited wrapper call, a mock or component timer that uses real delays, an arbitrary sleep, or an unmocked request. Control every async source, flush it explicitly, and assert final states after flushPromises.

open as a page