Vue Test Utils patterns - mount, shallowMount, wrapper API, trigger, setValue, flushPromises, testing composables, Pinia store mocking
Quick Guide:
mount()renders a component with its children;shallowMount()stubs them all and is the exception rather than the default. Query withget()when the element must exist andfind()when it may not, usingdata-testattributes rather than classes. Every DOM-updating method returns a promise and is awaited; anything Vue's reactivity does not track — a request, a timer — needsflushPromises()as well. A component's environment arrives through theglobalmounting options.
Detailed Resources:
get/find/findAll, trigger, setValueflushPromises, nextTick, debounced input, async setup() under Suspenseglobal seams: plugins, stubs, mocks, provide — and a custom mount that composes themmount it and drive the wrapper;
examples/core.md.setup() — it must be mounted inside Suspense, or it never
resolves; examples/async.md.<critical_requirements>
Await every DOM-updating method — trigger(), setValue(), setProps(), setData(). Each returns a promise that resolves after Vue has flushed, so an un-awaited call leaves the assertion looking at the pre-update DOM.
Call flushPromises() after anything Vue's reactivity does not own — a request, a timer callback, a promise chain started in onMounted. Awaiting the interaction flushes Vue's queue and nothing else.
Reach for mount() first, and treat shallowMount() as the exception. Stubbing every child changes how the component behaves, so a shallow test passes for a tree that does not render.
Select with data-test attributes. Classes and ids belong to styling and move under a restyle; a data-test attribute is a stated contract that a reader can see is load-bearing.
Supply the component's environment through the global mounting options — plugins, stubs, mocks, provide. That is the seam the library gives you, and it keeps the arrangement visible at the mount rather than hidden in module-level substitution.
</critical_requirements>
Auto-detection: @vue/test-utils, mount, shallowMount, VueWrapper, DOMWrapper, wrapper.get, wrapper.find, findAll, findComponent, findAllComponents, getComponent, trigger, setValue, setProps, setData, flushPromises, emitted, enableAutoUnmount, attachTo, renderStubDefaultSlot, config.global
Applies to:
Handled elsewhere:
describe/it, the assertion API, module substitution, mock
functions and fake timers all belong to whatever runs the fileglobal.plugins, and how to configure it is settled by whoever owns the storeVue's DOM updates are asynchronous, and the library's API is shaped around that one fact. trigger, setValue, setProps and setData all return promises for the same reason: the effect of the change is not observable on the line after it. Most flaky Vue component tests are a missing await on one of those four.
Two async queues, two tools. Awaiting a wrapper method flushes Vue's own render queue. flushPromises() drains the microtask queue, which is where a resolved request or a settled promise chain is waiting. A component that fetches on mount needs the second even though nothing was triggered.
Stubbing is a dial, not a switch. shallowMount is the far end of it — every child replaced, slots inert, integration coverage gone. Stubbing the one heavy child by name keeps the rest of the tree real and usually solves the actual problem, which is a chart library or a network call rather than depth.
import { mount, shallowMount } from "@vue/test-utils";
// Default: children render, slots work, events bubble
const wrapper = mount(TodoList, {
props: { todos: [{ id: 1, text: "Test", done: false }] },
});
// Every child stubbed - reach for this only when depth itself is the problem
const shallow = shallowMount(TodoList, { props: { todos: [] } });
// Usually better than shallow: stub the one child that misbehaves
const selective = mount(Dashboard, {
global: { stubs: { HeavyChartWidget: { template: "<div />" } } },
});
Full code: examples/core.md
get* throws with a useful message when the element is missing; find* returns an empty wrapper you then check with .exists(). The same split applies to getComponent / findComponent.
const input = wrapper.get('[data-test="search-input"]'); // must exist
expect(wrapper.find('[data-test="error"]').exists()).toBe(false); // may not exist
expect(wrapper.findAll('[data-test="result"]')).toHaveLength(3);
expect(wrapper.getComponent(ChildComponent).props("message")).toBe("Hello");
Using find() where get() was meant is the quiet failure: the empty wrapper satisfies nothing, and the assertion after it reports a confusing type error rather than "element not found".
Full code: examples/core.md
await wrapper.get('[data-test="email"]').setValue("test@example.com");
await wrapper.get('[data-test="form"]').trigger("submit.prevent");
expect(wrapper.emitted("submit")).toBeTruthy();
expect(wrapper.emitted("submit")![0]).toEqual([{ email: "test@example.com" }]);
emitted() accumulates across the whole test, so index into it rather than asserting on the array as a whole after several interactions.
Full code: examples/core.md
import { mount, flushPromises } from "@vue/test-utils";
const wrapper = mount(UserProfile, { props: { userId: 1 } });
expect(wrapper.find('[data-test="loading"]').exists()).toBe(true);
await flushPromises(); // the request settles, then Vue re-renders
expect(wrapper.get('[data-test="user-name"]').text()).toBe("John Doe");
flushPromises() drains microtasks only. A setTimeout needs the runner's fake timers advanced first, and then flushPromises() for whatever the callback started.
Full code: examples/async.md
Mount a component whose only job is to call the composable, so lifecycle hooks and inject work as they do in production.
function withSetup<T>(composable: () => T) {
let result!: T;
const wrapper = mount(
defineComponent({ setup: () => ((result = composable()), () => null) }),
);
return { result, unmount: () => wrapper.unmount() };
}
const { result, unmount } = withSetup(() =>
useLocalStorage("draft", "initial"),
);
Where the composable is inseparable from its UI, render a small template around it and assert on the DOM instead — the reactivity is then covered rather than assumed.
Full code: examples/composables.md
Four seams, each for a different kind of dependency, all set at the mount and all settable project-wide through config.global.
mount(Component, {
global: {
plugins: [
/* anything installed with app.use() - a store, a router, an i18n instance */
],
stubs: { RouterLink: true, HeavyChart: { template: "<div />" } },
mocks: { $t: (key: string) => key }, // global properties the template reads
provide: { [THEME_KEY]: { theme: ref("light") } }, // what an ancestor would provide
},
});
Wrap these in a custom mount once, so a test states only what makes it different.
Full code: examples/mocking.md
</patterns><red_flags>
Breaks at runtime:
trigger(), setValue(), setProps() or setData() without await — the assertion reads the DOM before Vue has flushed, so the test passes or fails by timingflushPromises() after a request resolves — the loading branch is still rendered when the assertion runssetData() on a component written with the Composition API — it only reaches an Options API data() function, so the call silently changes nothingsetup() mounted directly instead of inside Suspense — it never resolves and the wrapper stays on the fallbackisVisible() without attachTo: document.body — the element is not in a rendered document, so visibility cannot be computed correctlytrigger("click") on a disabled element — the browser drops it, and so does the library; that is correct behaviour rather than a bug to work aroundSurprising behaviour:
shallowMount stubs every child including ones from a component library, so a test can pass against a tree that renders nothingfind() returns an empty wrapper rather than throwing, which turns a missing element into a confusing error one line lateremitted() accumulates for the lifetime of the wrapper — asserting a length after several interactions counts all of themsetValue() applies only to <input>, <textarea> and <select>; on anything else it is a no-optrigger() already awaits nextTick, so an extra await nextTick() after it is redundantwrapper.vm to call a method or read state couples the test to the implementation; drive the component through the DOM insteadattachTo stays in the document until unmounted — register enableAutoUnmount once rather than remembering per test</red_flags>
npx skills add agents-inc/web-testing-vue-test-utils下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer