diff --git a/docs/README.skills.md b/docs/README.skills.md index 3ca10673..74e18c62 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -234,6 +234,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [typespec-api-operations](../skills/typespec-api-operations/SKILL.md) | Add GET, POST, PATCH, and DELETE operations to a TypeSpec API plugin with proper routing, parameters, and adaptive cards | None | | [typespec-create-agent](../skills/typespec-create-agent/SKILL.md) | Generate a complete TypeSpec declarative agent with instructions, capabilities, and conversation starters for Microsoft 365 Copilot | None | | [typespec-create-api-plugin](../skills/typespec-create-api-plugin/SKILL.md) | Generate a TypeSpec API plugin with REST operations, authentication, and Adaptive Cards for Microsoft 365 Copilot | None | +| [unit-test-vue-pinia](../skills/unit-test-vue-pinia/SKILL.md) | Write and review unit tests for Vue 3 + TypeScript + Vitest + Pinia codebases. Use when creating or updating tests for components, composables, and stores; mocking Pinia with createTestingPinia; applying Vue Test Utils patterns; and enforcing black-box assertions over implementation details. | `references/pinia-patterns.md` | | [update-avm-modules-in-bicep](../skills/update-avm-modules-in-bicep/SKILL.md) | Update Azure Verified Modules (AVM) to latest versions in Bicep files. | None | | [update-implementation-plan](../skills/update-implementation-plan/SKILL.md) | Update an existing implementation plan file with new or update requirements to provide new features, refactoring existing code or upgrading packages, design, architecture or infrastructure. | None | | [update-llms](../skills/update-llms/SKILL.md) | Update the llms.txt file in the root folder to reflect changes in documentation or specifications following the llms.txt specification at https://llmstxt.org/ | None | diff --git a/skills/unit-test-vue-pinia/SKILL.md b/skills/unit-test-vue-pinia/SKILL.md new file mode 100644 index 00000000..05ed6872 --- /dev/null +++ b/skills/unit-test-vue-pinia/SKILL.md @@ -0,0 +1,198 @@ +--- +name: unit-test-vue-pinia +category: testing +description: 'Write and review unit tests for Vue 3 + TypeScript + Vitest + Pinia codebases. Use when creating or updating tests for components, composables, and stores; mocking Pinia with createTestingPinia; applying Vue Test Utils patterns; and enforcing black-box assertions over implementation details.' +--- + +# unit-test-vue-pinia + +Use this skill to create or review unit tests for Vue components, composables, and Pinia stores. Keep tests small, deterministic, and behavior-first. + +## Workflow + +1. Identify the behavior boundary first: component UI behavior, composable behavior, or store behavior. +2. Choose the narrowest test style that can prove that behavior. +3. Set up Pinia with the least powerful option that still covers the scenario. +4. Drive the test through public inputs such as props, form updates, button clicks, emitted child events, and store APIs. +5. Assert observable outputs and side effects before considering any instance-level assertion. +6. Return or review tests with clear behavior-oriented names and note any remaining coverage gaps. + +## Core Rules + +- Test one behavior per test. +- Assert observable input/output behavior first (rendered text, emitted events, callback calls, store state changes). +- Avoid implementation-coupled assertions. +- Access `wrapper.vm` only in exceptional cases when there is no reasonable DOM, prop, emit, or store-level assertion. +- Prefer explicit setup in `beforeEach()` and reset mocks every test. +- Use checked-in reference material in `references/pinia-patterns.md` as the local source of truth for standard Pinia test setups. + +## Pinia Testing Approach + +Use `references/pinia-patterns.md` first, then fall back to Pinia's testing cookbook when the checked-in examples do not cover the case. + +### Default pattern for component tests + +Use `createTestingPinia` as a global plugin while mounting. +Prefer `createSpy: vi.fn` as the default for consistency and easier action-spy assertions. + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + }), + ], + }, +}); +``` + +By default, actions are stubbed and spied. +Use `stubActions: true` (default) when the test only needs to verify whether an action was called (or not called). + +### Accepted minimal Pinia setups + +The following are also valid and should not be flagged as incorrect: + +- `createTestingPinia({})` when the test does not assert Pinia action spy behavior. +- `createTestingPinia({ initialState: ... })` or `createTestingPinia({ stubActions: ... })` without `createSpy`, when the test only needs state seeding or action stubbing behavior and does not inspect generated spies. +- `setActivePinia(createTestingPinia(...))` in store/composable-focused tests (without mounting a component) when mocking/seeding dependent stores is needed. + +Use `createSpy: vi.fn` when action spy assertions are part of the test intent. + +### Execute real actions only when needed + +Use `stubActions: false` only when the test must validate the action's real behavior and side effects. Do not switch it on by default for simple "was called" assertions. + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + stubActions: false, + }), + ], + }, +}); +``` + +### Seed store state with `initialState` + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + initialState: { + counter: { n: 20 }, + user: { name: "Leia Organa" }, + }, + }), + ], + }, +}); +``` + +### Add Pinia plugins through `createTestingPinia` + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + plugins: [myPiniaPlugin], + }), + ], + }, +}); +``` + +### Getter override pattern for edge cases + +```ts +const pinia = createTestingPinia({ createSpy: vi.fn }); +const store = useCounterStore(pinia); + +store.double = 999; +// @ts-expect-error test-only reset of overridden getter +store.double = undefined; +``` + +### Pure store unit tests + +Prefer pure store tests with `createPinia()` when the goal is to validate store state transitions and action behavior without component rendering. Use `createTestingPinia()` only when you need stubbed dependent stores, seeded test doubles, or action spies. + +```ts +beforeEach(() => { + setActivePinia(createPinia()); +}); + +it("increments", () => { + const counter = useCounterStore(); + counter.increment(); + expect(counter.n).toBe(1); +}); +``` + +## Vue Test Utils Approach + +Follow Vue Test Utils guidance: + +- Mount shallow by default for focused unit tests. +- Mount full component trees only when integration behavior is the subject. +- Drive behavior through props, user-like interactions, and emitted events. +- Prefer `findComponent(...).vm.$emit(...)` for child stub events instead of touching parent internals. +- Use `nextTick` only when updates are async. +- Assert emitted events and payloads with `wrapper.emitted(...)`. +- Access `wrapper.vm` only when no DOM assertion, emitted event assertion, prop assertion, or store-level assertion can express the behavior. Treat it as an exception and keep the assertion narrowly scoped. + +## Key Testing Snippets + +Emit and assert payload: + +```ts +await wrapper.find("button").trigger("click"); +expect(wrapper.emitted("submit")?.[0]?.[0]).toBe("Mango Mission"); +``` + +Update input and assert output: + +```ts +await wrapper.find("input").setValue("Agent Violet"); +await wrapper.find("form").trigger("submit"); +expect(wrapper.emitted("save")?.[0]?.[0]).toBe("Agent Violet"); +``` + +## Test Writing Workflow + +1. Identify the behavior boundary to test. +2. Build minimal fixture data (only fields needed by that behavior). +3. Configure Pinia and required test doubles. +4. Trigger behavior through public inputs. +5. Assert public outputs and side effects. +6. Refactor test names to describe behavior, not implementation. + +## Constraints and Safety + +- Do not test private/internal implementation details. +- Do not overuse snapshots for dynamic UI behavior. +- Do not assert every field in large objects if only one behavior matters. +- Keep fake data deterministic; avoid random values. +- Do not claim a Pinia setup is wrong when it is one of the accepted minimal setups above. +- Do not rewrite working tests toward deeper mounting or real actions unless the behavior under test requires that extra surface area. +- Flag missing test coverage, brittle selectors, and implementation-coupled assertions explicitly during review. + +## Output Contract + +- For `create` or `update`, return the finished test code plus a short note describing the selected Pinia strategy. +- For `review`, return concrete findings first, then missing coverage or brittleness risks. +- When the safest choice is ambiguous, state the assumption that drove the chosen test setup. + +## References + +- `references/pinia-patterns.md` +- Pinia testing cookbook: +- Vue Test Utils guide: diff --git a/skills/unit-test-vue-pinia/references/pinia-patterns.md b/skills/unit-test-vue-pinia/references/pinia-patterns.md new file mode 100644 index 00000000..ceb63834 --- /dev/null +++ b/skills/unit-test-vue-pinia/references/pinia-patterns.md @@ -0,0 +1,95 @@ +# Pinia Testing Snippets (Cookbook-Aligned) + +Use these patterns directly when writing tests with `@pinia/testing`. + +## Component mount with `createTestingPinia` + +```ts +import { mount } from "@vue/test-utils"; +import { createTestingPinia } from "@pinia/testing"; +import { vi } from "vitest"; + +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + }), + ], + }, +}); +``` + +## Execute real actions + +Use this only when behavior inside the action must run. +If the test only checks call/no-call expectations, keep default stubbing (`stubActions: true`). + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + stubActions: false, + }), + ], + }, +}); +``` + +## Seed starting state + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + initialState: { + counter: { n: 10 }, + profile: { name: "Sherlock Holmes" }, + }, + }), + ], + }, +}); +``` + +## Use store in test and assert action call + +```ts +const pinia = createTestingPinia({ createSpy: vi.fn }); +const store = useCounterStore(pinia); + +store.increment(); +expect(store.increment).toHaveBeenCalledTimes(1); +``` + +## Add plugin under test + +```ts +const wrapper = mount(ComponentUnderTest, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + plugins: [myPiniaPlugin], + }), + ], + }, +}); +``` + +## Override and reset getters for edge tests + +```ts +const pinia = createTestingPinia({ createSpy: vi.fn }); +const store = useCounterStore(pinia); + +store.double = 42; +expect(store.double).toBe(42); + +// @ts-expect-error test-only reset +store.double = undefined; +```