Add unit-test-vue-pinia skill (#1005)

This commit is contained in:
Yauhen
2026-03-16 00:39:36 +01:00
committed by GitHub
parent 20afb96f66
commit 3751155165
3 changed files with 294 additions and 0 deletions

View File

@@ -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 |

View File

@@ -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: <https://test-utils.vuejs.org/guide/>
- 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: <https://pinia.vuejs.org/cookbook/testing.html>
- Vue Test Utils guide: <https://test-utils.vuejs.org/guide/>

View File

@@ -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;
```