dist/plugins/web-testing-vue-test-utils/skills/web-testing-vue-test-utils/SKILL.md
Vue Test Utils patterns - mount, shallowMount, wrapper API, trigger, setValue, flushPromises, testing composables, Pinia store mocking
npx skillsauth add agents-inc/skills web-testing-vue-test-utilsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Test Vue components with
mount()for full rendering orshallowMount()for isolation. Usewrapper.find()with data-test attributes,await trigger()for events,await setValue()for inputs. UseflushPromises()for async operations. Mock Pinia stores withcreateTestingPinia().
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST await all DOM-updating methods: trigger(), setValue(), setProps(), setData())
(You MUST use flushPromises() after async operations that Vue doesn't track (API calls, timers))
(You MUST use mount() by default - only use shallowMount() for performance issues or complex isolation)
(You MUST use data-test attributes for selectors, NOT classes or IDs)
(You MUST use createTestingPinia() for Pinia stores, NOT manual mocking)
</critical_requirements>
Auto-detection: Vue Test Utils, @vue/test-utils, mount, shallowMount, wrapper, VueWrapper, DOMWrapper, trigger, setValue, setProps, setData, flushPromises, findComponent, findAllComponents, createTestingPinia, @pinia/testing
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
examples/ folder:
Vue Test Utils provides utilities to mount Vue components in isolation and interact with them through the wrapper API. The guiding principle: "The more your tests resemble the way your software is used, the more confidence they can give you."
Core Principles:
mount() by default for realistic testing; shallowMount() only when neededdata-test attributes over classes or IDs that may changeWhen to use Vue Test Utils:
When NOT to use:
Use mount() for full rendering with child components. Use shallowMount() only when you need isolation or have performance issues.
import { mount, shallowMount } from "@vue/test-utils";
import { TodoList } from "./todo-list.vue";
// GOOD: mount() for realistic testing
const wrapper = mount(TodoList, {
props: {
todos: [{ id: 1, text: "Test", done: false }],
},
});
// Use when: You need to test component with its children
// Child components render normally, slots work, events bubble up
// shallowMount() for isolation (use sparingly)
const shallowWrapper = shallowMount(TodoList, {
props: {
todos: [{ id: 1, text: "Test", done: false }],
},
});
// Use when: Component has many heavy children, or you need complete isolation
// All child components are replaced with stubs
Why mount by default: shallowMount makes the component behave differently from production. Child components don't render, slots may not work correctly, and you lose integration coverage.
See examples/core.md for complete mounting examples.
Use get() when element must exist (throws on failure). Use find() when element may not exist (returns empty wrapper). Use findAll() for multiple elements.
import { mount } from "@vue/test-utils";
import { SearchForm } from "./search-form.vue";
const DATA_TEST_INPUT = '[data-test="search-input"]';
const DATA_TEST_BUTTON = '[data-test="search-button"]';
const DATA_TEST_RESULT = '[data-test="search-result"]';
test("renders search form elements", () => {
const wrapper = mount(SearchForm);
// get() - throws if not found (use for required elements)
const input = wrapper.get(DATA_TEST_INPUT);
const button = wrapper.get(DATA_TEST_BUTTON);
expect(input.exists()).toBe(true);
expect(button.text()).toBe("Search");
});
test("shows results after search", async () => {
const wrapper = mount(SearchForm);
// find() - returns empty wrapper if not found (use for conditional elements)
expect(wrapper.find(DATA_TEST_RESULT).exists()).toBe(false);
// After triggering search...
await wrapper.get(DATA_TEST_INPUT).setValue("test");
await wrapper.get(DATA_TEST_BUTTON).trigger("click");
// findAll() - returns array of wrappers
const results = wrapper.findAll(DATA_TEST_RESULT);
expect(results.length).toBeGreaterThan(0);
});
Why data-test attributes: CSS classes and IDs change during styling updates. data-test attributes are explicit testing contracts that don't affect production styling.
Always await DOM-updating methods. They return a Promise that resolves after Vue updates the DOM.
import { mount } from "@vue/test-utils";
import { LoginForm } from "./login-form.vue";
const DATA_TEST_EMAIL = '[data-test="email-input"]';
const DATA_TEST_PASSWORD = '[data-test="password-input"]';
const DATA_TEST_FORM = '[data-test="login-form"]';
const DATA_TEST_ERROR = '[data-test="error-message"]';
const VALID_EMAIL = "[email protected]";
const VALID_PASSWORD = "password123";
test("submits login form", async () => {
const wrapper = mount(LoginForm);
// setValue() for input values - MUST await
await wrapper.get(DATA_TEST_EMAIL).setValue(VALID_EMAIL);
await wrapper.get(DATA_TEST_PASSWORD).setValue(VALID_PASSWORD);
// trigger() for events - MUST await
await wrapper.get(DATA_TEST_FORM).trigger("submit.prevent");
// Assert emitted events
expect(wrapper.emitted("submit")).toBeTruthy();
expect(wrapper.emitted("submit")![0]).toEqual([
{ email: VALID_EMAIL, password: VALID_PASSWORD },
]);
});
test("shows validation errors", async () => {
const wrapper = mount(LoginForm);
// Submit empty form
await wrapper.get(DATA_TEST_FORM).trigger("submit.prevent");
// Check for error message
expect(wrapper.get(DATA_TEST_ERROR).text()).toContain("Email is required");
});
Why await: Vue updates the DOM asynchronously. Without await, assertions run before Vue finishes updating, causing flaky tests.
Use flushPromises() after async operations that Vue doesn't track (API calls, setTimeout, etc.). Use nextTick() only for reactive state updates.
import { mount, flushPromises } from "@vue/test-utils";
import { UserProfile } from "./user-profile.vue";
import { vi } from "vitest";
const DATA_TEST_LOADING = '[data-test="loading"]';
const DATA_TEST_NAME = '[data-test="user-name"]';
const DATA_TEST_ERROR = '[data-test="error"]';
const MOCK_USER = { id: 1, name: "John Doe" };
// Mock API module
vi.mock("@/api/users", () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from "@/api/users";
test("displays user data after loading", async () => {
// Arrange: Setup mock to resolve
vi.mocked(fetchUser).mockResolvedValue(MOCK_USER);
const wrapper = mount(UserProfile, {
props: { userId: 1 },
});
// Initially shows loading
expect(wrapper.find(DATA_TEST_LOADING).exists()).toBe(true);
// Act: Wait for all promises to resolve
await flushPromises();
// Assert: Loading gone, data displayed
expect(wrapper.find(DATA_TEST_LOADING).exists()).toBe(false);
expect(wrapper.get(DATA_TEST_NAME).text()).toBe("John Doe");
});
test("displays error on API failure", async () => {
// Arrange: Setup mock to reject
vi.mocked(fetchUser).mockRejectedValue(new Error("Network error"));
const wrapper = mount(UserProfile, {
props: { userId: 1 },
});
// Act: Wait for promise to reject
await flushPromises();
// Assert: Error displayed
expect(wrapper.get(DATA_TEST_ERROR).text()).toContain("Network error");
});
Why flushPromises: Vue's reactivity system doesn't track external promises (HTTP requests, timers). flushPromises() resolves all pending promises so the DOM reflects the async result.
See examples/async.md for complete async testing examples.
Use findComponent() or getComponent() to interact with child Vue components directly. Useful for testing component communication.
getComponent() - Throws error if not found (use when component must exist)findComponent() - Returns empty wrapper if not found (use when component may not exist)import { mount } from "@vue/test-utils";
import { ParentComponent } from "./parent-component.vue";
import { ChildComponent } from "./child-component.vue";
test("passes props to child component", () => {
const wrapper = mount(ParentComponent, {
props: { message: "Hello" },
});
// getComponent() - throws if not found (clearer error messages)
const child = wrapper.getComponent(ChildComponent);
// Assert props passed correctly
expect(child.props("message")).toBe("Hello");
});
test("receives emitted events from child", async () => {
const wrapper = mount(ParentComponent);
const child = wrapper.getComponent(ChildComponent);
// Trigger event on child
await child.vm.$emit("update", "new value");
// Assert parent handled the event
expect(wrapper.vm.value).toBe("new value");
});
test("conditionally rendered child component", () => {
const wrapper = mount(ParentComponent, {
props: { showChild: false },
});
// findComponent() - returns empty wrapper (check with .exists())
const child = wrapper.findComponent(ChildComponent);
expect(child.exists()).toBe(false);
});
test("finds component by name", () => {
const wrapper = mount(ParentComponent);
// Find by component name (less preferred - use component definition)
const child = wrapper.findComponent({ name: "ChildComponent" });
expect(child.exists()).toBe(true);
});
When to use getComponent vs findComponent:
getComponent when the child must exist - provides clearer error messages on failurefindComponent when the child may not exist - check with .exists() firstConfigure global plugins, components, and directives that all tests need. Create a custom mount function for consistency.
// test-utils.ts
import { mount, type MountingOptions, type VueWrapper } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import type { Component } from "vue";
// Import global components your app uses
import { Button } from "@/components/ui/button.vue";
import { Input } from "@/components/ui/input.vue";
interface ExtendedMountOptions extends MountingOptions<unknown> {
initialPiniaState?: Record<string, unknown>;
}
function customMount<T extends Component>(
component: T,
options: ExtendedMountOptions = {},
): VueWrapper {
const { initialPiniaState, ...mountOptions } = options;
return mount(component, {
global: {
plugins: [
createTestingPinia({
initialState: initialPiniaState,
stubActions: false,
}),
],
components: {
Button,
Input,
},
stubs: {
// Stub router components by default
RouterLink: true,
RouterView: true,
},
...mountOptions.global,
},
...mountOptions,
});
}
export { customMount as mount };
export * from "@vue/test-utils";
Why custom mount: Avoids repeating global configuration in every test. Creates consistent test environment matching your app.
See examples/mocking.md for complete mocking examples.
</patterns><red_flags>
High Priority Issues:
shallowMount by default - Reduces test fidelity; child components don't render as expectedflushPromises() after API calls - Assertions run before async operations completeMedium Priority Issues:
wrapper.vm directly instead of testing user-visible behaviorcreateTestingPinia() - Manual store mocking is error-prone and verbosewrapper.setData() to set reactive state - Better to trigger through user interactionsCommon Mistakes:
await on trigger(), setValue(), setProps(), setData()find() instead of get() for required elements (hides failures)wrapper.vm.someMethod() instead of triggering through UIGotchas and Edge Cases:
trigger('click') doesn't work on disabled elements (expected browser behavior)setValue() only works on <input>, <textarea>, and <select> elementsshallowMount stubs ALL child components including those from component librariesflushPromises() only resolves Promises - not setTimeout (use fake timers)setData() does NOT work with Composition API - it only works with Options API data() functionisVisible() requires attachTo: document.body to work correctly</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST await all DOM-updating methods: trigger(), setValue(), setProps(), setData())
(You MUST use flushPromises() after async operations that Vue doesn't track (API calls, timers))
(You MUST use mount() by default - only use shallowMount() for performance issues or complex isolation)
(You MUST use data-test attributes for selectors, NOT classes or IDs)
(You MUST use createTestingPinia() for Pinia stores, NOT manual mocking)
Failure to follow these rules will produce flaky tests that don't reflect real component behavior.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety