seed-skills/vue-testing-utils/SKILL.md
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
npx skillsauth add PramodDutta/qaskills Vue Testing 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.
This skill makes an AI agent write Vue 3 component tests with @vue/test-utils on Vitest: mounting with props and slots, querying via data-testid, triggering events and awaiting the render queue, asserting emitted() payloads, and wiring createTestingPinia and mocked routers through global.plugins. Trigger it on any Vue 3 + Vite project where components need unit or integration tests.
mount, reach for shallowMount rarely. Stubbing all children tests a skeleton, not the component. Shallow-render only when a child is genuinely heavy (charts, maps) — and stub that one child explicitly instead.wrapper.vm internals or assert on ref values; those tests survive refactors only by accident.await every interaction. Vue batches DOM updates; trigger, setValue, and setProps all return promises that resolve after the next tick. A missing await asserts against the stale DOM.data-testid or roles, not class selectors. Tailwind/scoped-CSS classes change with styling work; test IDs change only when behavior does.wrapper.emitted('save') returns the calls array; check both that it fired and what it carried.createTestingPinia({ stubActions: false }) plus mocked HTTP you test store-component integration honestly; stubbed actions are for pure-render tests only.npm install --save-dev @vue/test-utils vitest jsdom @pinia/testing @vitejs/plugin-vue
// vitest.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
include: ['src/**/*.test.ts'],
restoreMocks: true,
},
});
A component worth testing:
<!-- src/components/QuantityPicker.vue -->
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(defineProps<{ modelValue: number; max?: number }>(), { max: 10 });
const emit = defineEmits<{ 'update:modelValue': [value: number] }>();
const atMax = computed(() => props.modelValue >= props.max);
function increment(): void {
if (!atMax.value) emit('update:modelValue', props.modelValue + 1);
}
</script>
<template>
<div>
<span data-testid="qty">{{ modelValue }}</span>
<button data-testid="inc" :disabled="atMax" @click="increment">+</button>
</div>
</template>
The test:
// src/components/QuantityPicker.test.ts
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import QuantityPicker from './QuantityPicker.vue';
describe('QuantityPicker', () => {
it('emits update:modelValue with the incremented quantity', async () => {
const wrapper = mount(QuantityPicker, { props: { modelValue: 2 } });
await wrapper.find('[data-testid="inc"]').trigger('click');
expect(wrapper.emitted('update:modelValue')).toEqual([[3]]);
});
it('disables the button at max and emits nothing on click', async () => {
const wrapper = mount(QuantityPicker, { props: { modelValue: 5, max: 5 } });
const button = wrapper.find('[data-testid="inc"]');
expect(button.attributes('disabled')).toBeDefined();
await button.trigger('click');
expect(wrapper.emitted('update:modelValue')).toBeUndefined();
});
it('re-renders when the parent updates the prop', async () => {
const wrapper = mount(QuantityPicker, { props: { modelValue: 1 } });
await wrapper.setProps({ modelValue: 7 });
expect(wrapper.get('[data-testid="qty"]').text()).toBe('7');
});
});
import { mount } from '@vue/test-utils';
import { expect, it } from 'vitest';
import LoginForm from './LoginForm.vue';
it('submits trimmed credentials as the submit event payload', async () => {
const wrapper = mount(LoginForm);
await wrapper.get('[data-testid="email"]').setValue(' [email protected] ');
await wrapper.get('[data-testid="password"]').setValue('hunter2hunter2');
await wrapper.get('form').trigger('submit.prevent');
expect(wrapper.emitted('submit')).toEqual([
[{ email: '[email protected]', password: 'hunter2hunter2' }],
]);
});
import { flushPromises, mount } from '@vue/test-utils';
import { expect, it, vi } from 'vitest';
import SkillList from './SkillList.vue';
import * as api from '../api/skills';
it('renders fetched skills after the loading state', async () => {
vi.spyOn(api, 'fetchSkills').mockResolvedValue([
{ slug: 'vitest-testing', name: 'Vitest' },
{ slug: 'msw-mocking', name: 'MSW' },
]);
const wrapper = mount(SkillList);
expect(wrapper.find('[data-testid="spinner"]').exists()).toBe(true);
await flushPromises(); // resolves the fetch AND the subsequent render
expect(wrapper.find('[data-testid="spinner"]').exists()).toBe(false);
expect(wrapper.findAll('[data-testid="skill-row"]')).toHaveLength(2);
expect(wrapper.text()).toContain('Vitest');
});
import { createTestingPinia } from '@pinia/testing';
import { mount } from '@vue/test-utils';
import { expect, it, vi } from 'vitest';
import CartBadge from './CartBadge.vue';
import { useCartStore } from '../stores/cart';
it('shows the item count from the store and calls clear on click', async () => {
const wrapper = mount(CartBadge, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
initialState: { cart: { items: [{ sku: 'A1' }, { sku: 'B2' }] } },
}),
],
},
});
const store = useCartStore(); // the same instance the component uses
expect(wrapper.get('[data-testid="count"]').text()).toBe('2');
await wrapper.get('[data-testid="clear"]').trigger('click');
expect(store.clear).toHaveBeenCalledOnce(); // actions auto-stubbed as spies
});
import { mount } from '@vue/test-utils';
import { expect, it, vi } from 'vitest';
import SkillCard from './SkillCard.vue';
it('navigates to the skill detail page on card click', async () => {
const push = vi.fn();
const wrapper = mount(SkillCard, {
props: { slug: 'supertest-api', name: 'SuperTest' },
global: {
mocks: { $router: { push } },
stubs: { RouterLink: { template: '<a><slot /></a>' } },
},
});
await wrapper.get('[data-testid="card"]').trigger('click');
expect(push).toHaveBeenCalledWith({ name: 'skill-detail', params: { slug: 'supertest-api' } });
});
import { mount } from '@vue/test-utils';
import { expect, it } from 'vitest';
import DataTable from './DataTable.vue';
it('renders the scoped row slot with each item', () => {
const wrapper = mount(DataTable, {
props: { items: [{ id: 1, name: 'alpha' }] },
slots: {
row: `<template #row="{ item }"><td data-testid="cell">{{ item.name }}</td></template>`,
},
});
expect(wrapper.get('[data-testid="cell"]').text()).toBe('alpha');
});
wrapper.get() when the element must exist (throws with a clear message); wrapper.find() + .exists() only for asserting absence.const factory = (props = {}) => mount(Comp, { props: { ...defaults, ...props } }) — not via a mutable module-level wrapper.emitted() payload equality on the whole calls array (toEqual([[3]])) to catch double-fires for free.<Teleport>, target the teleport destination with document.querySelector or stub teleport with global.stubs: { teleport: true }.attributes('aria-expanded'), attributes('disabled') — these are behavior, not styling.restoreMocks: true plus fresh mounts eliminates 90% of cross-test pollution.wrapper.vm.someRef = 5 to set state. Mutating internals bypasses the component contract; drive state through props, interactions, or store initial state.await on trigger/setValue/setProps. The assertion sees the previous DOM and passes or fails for the wrong reason.shallowMount as the default everywhere. Stub names in snapshots (<child-component-stub>) verify nothing about integration.await router.isReady() for a unit test. Mock $router.push instead; real-router tests belong in a small dedicated navigation suite.expect(wrapper.classes()).toContain('text-red-500')). Assert the state that drives the class (aria-invalid, emitted validation event) instead.beforeEach that mounts with a kitchen-sink global config for every test in the file — slot, store, and router config should appear in the tests that need them.@vue/test-utils is in devDependencies.await/flushPromises or pollute each other through shared wrappers.createLocalVue, propsData) tests to the Vue 3 global/props API.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.