Vue test utils
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill vue-test-utilsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Vue.js component testing using Vue Test Utils with mount/shallow mount, event simulation, Vuex/Pinia store testing, and composition API testing.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.5 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
Vue Test Utils Testing
This skill makes the agent write Vue 3 component tests with Vue Test Utils (VTU) + Vitest that assert on rendered output and user-observable behavior, not implementation internals. Trigger it whenever you see .vue SFCs, @vue/test-utils, mount/shallowMount, Pinia/Vuex stores under test, or a Vitest config in a Vue project.
Core Principles
- Prefer
mountovershallowMount. Fullmountrenders children so you test real behavior. Reach forshallowMountonly to isolate a component from an expensive/irrelevant child — and know that stubbing children hides integration bugs. - Query by accessible roles and
data-testid, not by CSS classes. Classes are styling and churn constantly;find('[data-testid="submit"]')andgetByRolesurvive refactors and assert what users actually see. awaitevery state change. Vue's DOM updates are asynchronous. Aftertrigger,setValue,setProps, or a store mutation you mustawait wrapper.vm.$nextTick()(orawait trigger(...), which returns nextTick) before asserting, or you assert against stale DOM.- Test the component's contract: props in, events/DOM out. Assert emitted events with
wrapper.emitted(), assert rendered text/attributes, and pass props. Do not assert on privatedata/refs or call internal methods. - Use a real Pinia instance with
createTestingPinia, not hand-mocked stores. It gives you real getters, auto-spied actions, andinitialState— far more faithful than stubbing the store object. - Stub the network, render the component. Mock
fetch/axios at the module boundary withvi.mock; never let component tests hit a live API.
Setup
npm install -D vitest @vue/test-utils@2 @vitest/coverage-v8 jsdom \
@pinia/testing pinia
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true, // describe/it/expect without imports
setupFiles: ['./tests/setup.ts'],
coverage: { provider: 'v8', reporter: ['text', 'html'] },
},
});
// tests/setup.ts
import { config } from '@vue/test-utils';
// Global stubs/plugins for every test, e.g. stub <RouterLink>
config.global.stubs = { RouterLink: { template: '<a><slot /></a>' } };
Patterns / Workflow
1. Render and assert on output
// Counter.vue exposes a button that increments a label
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from '@/components/Counter.vue';
describe('Counter', () => {
it('renders the initial count from a prop', () => {
const wrapper = mount(Counter, { props: { start: 3 } });
expect(wrapper.get('[data-testid="count"]').text()).toBe('3');
});
it('increments when the button is clicked', async () => {
const wrapper = mount(Counter, { props: { start: 0 } });
await wrapper.get('[data-testid="increment"]').trigger('click');
expect(wrapper.get('[data-testid="count"]').text()).toBe('1');
});
});
2. Form input, v-model, and emitted events
import { mount } from '@vue/test-utils';
import SearchBox from '@/components/SearchBox.vue';
it('emits "search" with the trimmed query on submit', async () => {
const wrapper = mount(SearchBox);
await wrapper.get('input[type="search"]').setValue(' vue testing ');
await wrapper.get('form').trigger('submit.prevent');
// emitted() returns arrays of arrays: one entry per emission
const events = wrapper.emitted('search');
expect(events).toHaveLength(1);
expect(events![0]).toEqual(['vue testing']);
});
it('disables the button while the query is empty', async () => {
const wrapper = mount(SearchBox);
const button = wrapper.get('button[type="submit"]');
expect(button.attributes('disabled')).toBeDefined();
await wrapper.get('input[type="search"]').setValue('x');
expect(button.attributes('disabled')).toBeUndefined();
});
3. Props, slots, and conditional rendering
import { mount } from '@vue/test-utils';
import Alert from '@/components/Alert.vue';
it('renders the danger variant and the default slot', () => {
const wrapper = mount(Alert, {
props: { variant: 'danger' },
slots: { default: 'Something broke' },
});
expect(wrapper.classes()).toContain('alert--danger');
expect(wrapper.text()).toContain('Something broke');
});
it('reacts to a prop change', async () => {
const wrapper = mount(Alert, { props: { variant: 'info' } });
await wrapper.setProps({ variant: 'danger' });
expect(wrapper.classes()).toContain('alert--danger');
});
it('hides itself when "open" is false', () => {
const wrapper = mount(Alert, { props: { open: false } });
expect(wrapper.find('[data-testid="alert"]').exists()).toBe(false);
});
4. Testing a component backed by Pinia
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { vi } from 'vitest';
import CartSummary from '@/components/CartSummary.vue';
import { useCartStore } from '@/stores/cart';
it('shows the item count from the store and dispatches checkout', async () => {
const wrapper = mount(CartSummary, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn, // actions are auto-stubbed + spied
initialState: { cart: { items: [{ id: 1 }, { id: 2 }] } },
}),
],
},
});
expect(wrapper.get('[data-testid="count"]').text()).toBe('2');
const store = useCartStore();
await wrapper.get('[data-testid="checkout"]').trigger('click');
expect(store.checkout).toHaveBeenCalledOnce();
});
it('updates the DOM when a getter-backed value changes', async () => {
const wrapper = mount(CartSummary, {
global: { plugins: [createTestingPinia({ createSpy: vi.fn })] },
});
const store = useCartStore();
store.items.push({ id: 9 }); // mutate real state
await wrapper.vm.$nextTick();
expect(wrapper.get('[data-testid="count"]').text()).toBe('1');
});
5. Async component with a mocked fetch
import { mount, flushPromises } from '@vue/test-utils';
import { vi, beforeEach } from 'vitest';
import UserCard from '@/components/UserCard.vue';
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 1, name: 'Ada Lovelace' }),
} as Response);
});
it('renders a loading state then the fetched user', async () => {
const wrapper = mount(UserCard, { props: { userId: 1 } });
expect(wrapper.text()).toContain('Loading');
await flushPromises(); // resolve fetch + re-render
expect(wrapper.text()).toContain('Ada Lovelace');
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
it('shows an error message when the request fails', async () => {
(global.fetch as any).mockResolvedValueOnce({ ok: false, status: 500 });
const wrapper = mount(UserCard, { props: { userId: 1 } });
await flushPromises();
expect(wrapper.get('[data-testid="error"]').text()).toMatch(/failed/i);
});
6. Testing a composable in isolation
Composables that use lifecycle hooks (onMounted) need a host component; pure ones can be called directly.
import { withSetup } from './withSetup'; // tiny mount helper
import { useCounter } from '@/composables/useCounter';
it('increments and exposes a reactive count', async () => {
const [result, app] = withSetup(() => useCounter(5));
expect(result.count.value).toBe(5);
result.increment();
expect(result.count.value).toBe(6);
app.unmount();
});
// tests/withSetup.ts — runs a composable inside a real app instance
import { createApp } from 'vue';
export function withSetup<T>(composable: () => T): [T, ReturnType<typeof createApp>] {
let result!: T;
const app = createApp({ setup() { result = composable(); return () => {}; } });
app.mount(document.createElement('div'));
return [result, app];
}
Best Practices
- Add
data-testidto elements you assert on. It decouples tests from markup/classes and makes intent explicit. - Use
wrapper.get()when an element must exist (it throws a clear error if missing) andwrapper.find().exists()when checking for absence. - Prefer
findComponent(ChildStub)with aname/refover CSS selectors when asserting child props:wrapper.findComponent(ProductCard).props('price'). - Reset mocks between tests with
vi.clearAllMocks()inafterEach(orclearMocks: truein config) so spy call counts don't leak. - Use
createTestingPinia({ stubActions: false })when you need actions to actually run (e.g. testing a store-driven flow end to end). - Test the rendered text/role a user would perceive, then layer in emitted-event assertions for the parent contract.
Anti-Patterns
- Forgetting
awaitafter a state change.wrapper.trigger('click'); expect(...)asserts before Vue re-renders and gives flaky, confusing failures. Alwaysawaitthe trigger/$nextTick/flushPromises. - Asserting on internal
wrapper.vmdata or calling private methods. Tests coupled to implementation break on every refactor. Drive via the DOM and assert via the DOM/emitted events. - Selecting by CSS class.
find('.btn-primary')shatters the moment a designer renames a class. Usedata-testidor roles. - Defaulting to
shallowMounteverywhere. Stubbing all children means you never test that the pieces actually work together; bugs slip through the seams. - Hand-rolling a fake store object. It drifts from the real store's getters/actions. Use
createTestingPiniaso getters compute and actions are spied for free. - Letting tests hit a real API or a real router. Mock
fetch/axios withvi.mockand stubRouterLink/router-view; otherwise tests are slow, flaky, and network-dependent.
When to Trigger This Skill
- "Write Vue component tests" / "test this
.vuecomponent" / "add unit tests for my Vue app" - Anything mentioning
@vue/test-utils,mount,shallowMount,flushPromises,createTestingPinia - "Test my Pinia / Vuex store" or "test this composable"
- "My Vue test is flaky / asserts the wrong DOM" (usually a missing
await) - Setting up Vitest + jsdom for a Vue 3 project