mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-16 16:36:53 +08:00
Add rules/vue/testing.md: - Vitest + Vue Test Utils + @pinia/testing stack - Component mounting (mount vs shallowMount), stubs and mocks - Composable testing with effectScope and mountComposable helper - Pinia store testing pattern (setActivePinia + ) - Vue Router testing with createMemoryHistory - Async assertion pitfalls (flushPromises/nextTick) - Testing implementation details vs rendered output - Coverage thresholds: 80%+ for composables/stores, smoke tests for components - Vitest configuration with jsdom environment and v8 coverage
7.4 KiB
7.4 KiB
paths
| paths | |||||
|---|---|---|---|---|---|
|
Vue Testing
This file extends typescript/testing.md, common/testing.md, and react/testing.md with Vue-specific testing guidance.
Testing Stack
- Vitest as the test runner (fast, Vite-native, Jest-compatible API).
- Vue Test Utils (
@vue/test-utils) for component mounting and interaction. - @pinia/testing for mocking Pinia stores in tests.
- @testing-library/vue when preferring user-centric queries over component internals.
- Playwright / Cypress for end-to-end tests.
npm install -D vitest @vue/test-utils jsdom
Test File Location
src/components/UserCard/
UserCard.vue
UserCard.test.ts # co-located
index.ts
Or:
src/components/UserCard.vue
src/components/__tests__/UserCard.test.ts
Follow the project's existing convention consistently.
Component Testing
Mounting
import { mount, shallowMount } from "@vue/test-utils";
import UserCard from "./UserCard.vue";
import { createPinia, setActivePinia } from "pinia";
describe("UserCard", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("renders user name", () => {
const wrapper = mount(UserCard, {
props: {
user: { id: "1", name: "Alice" },
},
});
expect(wrapper.text()).toContain("Alice");
});
it("emits select event on click", async () => {
const wrapper = mount(UserCard, {
props: {
user: { id: "1", name: "Alice" },
},
});
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")).toBeTruthy();
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
});
shallowMount vs mount
shallowMount: Stubs all child components — faster, tests the component in isolation. Useful for unit tests.mount: Renders the full tree — tests integration. Use for critical user-facing component tests.
// Unit test — use shallowMount
const wrapper = shallowMount(UserCard, { props: { user } });
// Only UserCard renders; child components are stubs
// Integration test — use mount
const wrapper = mount(Form, {
global: {
plugins: [createPinia()],
},
});
// Full rendering tree
Stubs and Mocks
// Stub a child component
const wrapper = mount(Parent, {
global: {
stubs: {
HeavyChart: true, // renders as empty placeholder
UserAvatar: {
template: '<div class="mock-avatar" />',
},
},
},
});
// Mock a composable
vi.mock("@/composables/useUser", () => ({
useUser: () => ({
user: ref({ id: "1", name: "Test" }),
isLoading: ref(false),
}),
}));
// Mock a Pinia store
const store = useUserStore();
store.$patch({ currentUser: { id: "1", name: "Test" } });
Composable Testing
import { useCounter } from "./useCounter";
import { createApp, ref } from "vue";
// With effectScope for cleanup
import { effectScope } from "vue";
describe("useCounter", () => {
it("increments count", () => {
const scope = effectScope();
const { count, increment } = scope.run(() => useCounter())!;
expect(count.value).toBe(0);
increment();
expect(count.value).toBe(1);
scope.stop(); // cleanup watchers
});
});
For composables with lifecycle hooks, mount inside a component:
import { mount } from "@vue/test-utils";
import { defineComponent, h } from "vue";
function mountComposable<T>(composable: () => T) {
let result: T;
const TestComponent = defineComponent({
setup() { result = composable(); },
render: () => h("div"),
});
mount(TestComponent);
return result!;
}
it("useEventListener attaches listener", () => {
const handler = vi.fn();
mountComposable(() => useEventListener("click", handler));
window.dispatchEvent(new Event("click"));
expect(handler).toHaveBeenCalled();
});
Pinia Store Testing
import { setActivePinia, createPinia } from "pinia";
import { useUserStore } from "./useUserStore";
describe("useUserStore", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("fetches user and updates state", async () => {
vi.mocked(getUser).mockResolvedValue({ id: "1", name: "Alice" });
const store = useUserStore();
await store.fetchUser("1");
expect(store.currentUser).toEqual({ id: "1", name: "Alice" });
expect(store.isLoading).toBe(false);
expect(store.error).toBeNull();
});
it("handles fetch error", async () => {
vi.mocked(getUser).mockRejectedValue(new Error("Not found"));
const store = useUserStore();
await store.fetchUser("999");
expect(store.currentUser).toBeNull();
expect(store.isLoading).toBe(false);
expect(store.error).toBeInstanceOf(Error);
});
});
Vue Router Testing
import { createRouter, createWebHistory } from "vue-router";
import { mount } from "@vue/test-utils";
import UserPage from "./UserPage.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/users/:id", component: UserPage, props: true },
],
});
// Navigate before mounting
await router.push("/users/42");
await router.isReady();
const wrapper = mount(UserPage, {
global: { plugins: [router] },
});
- Use
createMemoryHistory()for tests to avoid URL state leaks. - Always
await router.isReady()before asserting.
What to Test
| Level | What | Example |
|---|---|---|
| Unit | Composables, utility functions, store actions | useCounter().increment() works |
| Component | Rendering, props, events, slots | UserCard renders name, emits on click |
| Integration | Multiple components, router, store together | Form submits, store updates, route navigates |
| E2E | Critical user flows | Login → dashboard → create item |
Coverage Thresholds
- Unit tests: 80%+ for composables and store logic.
- Component tests: every component with logic beyond pure presentation should have at least a smoke test.
- E2E: at minimum, login, signup, and the primary user journey.
Common Pitfalls
Async Assertions
// WRONG: assertion not awaited
it("loads user", () => {
const wrapper = mount(UserCard, { props: { userId: "1" } });
expect(wrapper.text()).toContain("Alice"); // fetch hasn't resolved yet
});
// CORRECT: wait for DOM updates
it("loads user", async () => {
const wrapper = mount(UserCard, { props: { userId: "1" } });
await flushPromises(); // or nextTick / waitFor
expect(wrapper.text()).toContain("Alice");
});
Not Cleaning Up
Vitest auto-cleans with vi.restoreAllMocks(), but manual timers and subscriptions need explicit cleanup:
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
Testing Implementation Details
// WRONG: testing internal ref name
expect(wrapper.vm._internalCounter).toBe(5);
// CORRECT: testing rendered output or public API
expect(wrapper.find(".count-display").text()).toBe("5");
Vitest Configuration
// vitest.config.ts
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.{vue,ts}"],
},
},
});