From fb5d9d0eea4318009ba8f5a8dfb25a7007a30caa Mon Sep 17 00:00:00 2001 From: Bujidao <3317431882@qq.com> Date: Fri, 12 Jun 2026 17:54:09 +0800 Subject: [PATCH] feat(rules): add Vue testing rules 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 --- rules/vue/testing.md | 311 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 rules/vue/testing.md diff --git a/rules/vue/testing.md b/rules/vue/testing.md new file mode 100644 index 00000000..21375442 --- /dev/null +++ b/rules/vue/testing.md @@ -0,0 +1,311 @@ +--- +paths: + - "**/*.vue" + - "**/*.test.ts" + - "**/*.spec.ts" + - "**/composables/**/*.ts" + - "**/stores/**/*.ts" +--- +# Vue Testing + +> This file extends [typescript/testing.md](../typescript/testing.md), [common/testing.md](../common/testing.md), and [react/testing.md](../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. + +```bash +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 + +```ts +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. + +```ts +// 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 + +```ts +// Stub a child component +const wrapper = mount(Parent, { + global: { + stubs: { + HeavyChart: true, // renders as empty placeholder + UserAvatar: { + template: '
', + }, + }, + }, +}); + +// 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 + +```ts +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: + +```ts +import { mount } from "@vue/test-utils"; +import { defineComponent, h } from "vue"; + +function mountComposable