一键导入
unit-test-writing
This explains how to write unit test for this project. When a user asks to write unit test, you MUST read this before writing the tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
This explains how to write unit test for this project. When a user asks to write unit test, you MUST read this before writing the tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.
Create or update Jira issues for EN (Engineering) or AW (Apheris Web) boards interactively. Use this skill when: create issue, update issue, new EN ticket, new AW ticket, jira issue, engineering ticket, create task, fix jira formatting
Control Home Assistant devices, lights, switches, and media. Look up room/device states. Use when the user asks to turn on/off lights, check sensors, control media players, or interact with any smart home device. Also covers the WLED Schreibtisch strip (palettes, presets, PIN unlock).
Guide for writing Open Knowledge Format (OKF) v0.1 documents. Use this skill when: 'write okf' 'okf document' 'knowledge bundle' 'okf spec' 'create concept'
Guide for using and migrating to the semantic color system. Use this skill when migrating components from hardcoded colors, creating new styled components, reviewing color consistency, or updating theme-related code.
Search, read, and write Confluence pages as Markdown. Use this skill when: confluence, wiki page, find a doc, search confluence, read confluence page, publish to confluence, update wiki, confluence URL, atlassian wiki
| name | unit-test-writing |
| description | This explains how to write unit test for this project. When a user asks to write unit test, you MUST read this before writing the tests. |
This document outlines the common patterns, conventions, and best practices found in the UI test files.
import { mountSuspended } from '@nuxt/test-utils/runtime';
// ui/app/components/ App components - use #components alias
import { ComponentName } from '#components';
// ui/layers/base/components Base layer - use relative imports
import ComponentName from '../ComponentName.vue';
// For all components
describe('~/components/ComponentName.vue', () => {
// For stores
describe('~/store/useStoreName', () => {
// Time mocking (when needed)
const date = new Date(2025, 6, 1, 13);
vi.setSystemTime(date);
vi.hoisted() for mocks that need to have different values in multiple testsbeforeEach when needed// API request mocking
const { requestMock } = vi.hoisted(() => ({ requestMock: vi.fn() }));
mockNuxtImport('request', () => requestMock);
describe('~/components/Component.vue', () => {
// Reset hoisted mocks in beforeEach
beforeEach(() => {
requestMock.mockReset();
});
it('test something', async () => {
requestMock.mockReturnValueOnce({ data: {});
// mountSuspended and assertions...
})
it('test other thing', async () => {
requestMock.mockReturnValueOnce({ error: {});
// mountSuspended and assertions...
})
// Navigation mocking
const { navigateToMock } = vi.hoisted(() => ({ navigateToMock: vi.fn() }));
mockNuxtImport('navigateTo', () => navigateToMock);
// Runtime configuration mocking
mockNuxtImport('useRuntimeConfig', () => {
return () => ({
public: { APHERIS_API_URL: 'http://test.url' },
});
});
it('renders the component', async () => {
const wrapper = await mountSuspended(ComponentName, {
props: {
modelValue: 'test value',
label: 'test label',
hint: 'test hint',
},
});
await expect(wrapper.html()).toMatchFileSnapshot(
'./__snapshots__/ComponentName.html',
);
});
// Test with various prop combinations
it('renders with custom props', async () => {
const wrapper = await mountSuspended(ComponentName, {
props: {
modelValue: 'test value',
label: 'test label',
hint: 'test hint',
},
});
// assertions...
});
// Test default props
it('renders with default values', async () => {
const wrapper = await mountSuspended(ComponentName);
// assertions...
});
it('emits event when clicked', async () => {
const wrapper = await mountSuspended(ComponentName, {
props: { show: true },
});
await wrapper.find('[data-test="close-button"]').trigger('click');
expect(wrapper.emitted('close')).toBeTruthy();
expect(wrapper.emitted('close')?.length).toBe(1);
});
it('fetchModels', async () => {
requestMock.mockReturnValueOnce({
data: { data: testData },
});
await store.fetchModels();
expect(store.storeModels[testData[0]!.id]).toStrictEqual(testData[0]);
expect(requestMock.mock.calls[0]).toMatchInlineSnapshot(
`["/api/v1/endpoint"]`,
);
});
it('installApplication shows success message', async () => {
requestMock.mockReturnValueOnce({ data: { status: 'success' } });
const showMessageMock = vi.fn();
useMessageStore().showMessage = showMessageMock;
await store.installApplication('app-id', 'v1.0.0');
expect(showMessageMock).toHaveBeenCalledWith({
type: 'success',
body: 'Application app-id installed successfully.',
});
});
import { mockComponent } from '@nuxt/test-utils/runtime';
mockComponent('ChildComponent', {
template: '<div>ChildComponent</div>',
});
import { mockNuxtImport } from '@nuxt/test-utils/runtime';
mockNuxtImport('useRuntimeConfig', () => {
return () => ({
public: { APHERIS_API_URL: 'http://test.url' },
});
});
it('renders with pinia state', async () => {
useStore().someState = true;
useStore().otherState = { foo: 'bar' };
const wrapper = await mountSuspended(ComponentName);
// assertions...
});
// Using beforeEach for shared state setup
describe('with pinia state', () => {
beforeEach(() => {
useStore().someState = true;
useStore().otherState = { foo: 'bar' };
});
it('renders correctly with state', async () => {
const wrapper = await mountSuspended(ComponentName);
// assertions...
});
it('handles state changes', async () => {
const wrapper = await mountSuspended(ComponentName);
// additional assertions...
});
});
const mockMethod = vi.fn();
useStore().methodName = mockMethod;
// Test the mock was called correctly
expect(mockMethod.mock.calls[0]).toMatchInlineSnapshot(
`["expected", "params"]`,
);
// File snapshots (preferred for full component HTML)
// IMPORTANT: Always use .html suffix for component snapshots, NOT .snap
await expect(wrapper.html()).toMatchFileSnapshot(
'./__snapshots__/ComponentName.html',
);
// Inline snapshots (for specific content like function calls or small strings)
expect(element.html()).toMatchInlineSnapshot(`"expected html content"`);
expect(mockFunction.mock.calls[0]).toMatchInlineSnapshot(
`["param1", "param2"]`,
);
// Using data-test attributes (preferred)
const element = wrapper.find('[data-test="element-name"]');
expect(element.exists()).toBe(true);
expect(element.text()).toBe('expected text');
// Multiple elements
const elements = wrapper.findAll('[data-test="list-item"]');
expect(elements.map((el) => el.text())).toMatchInlineSnapshot(`[...]`);
// Test element doesn't exist
const element = wrapper.find('[data-test="conditional-element"]');
expect(element.exists()).toBe(false);
// Test v-if comment
expect(wrapper.html()).toBe('<!--v-if-->');
ui/
├── app/components/__tests__/
├── layers/base/components/__tests__/
├── app/stores/__tests__/
└── tests/assets/ # Shared test data
__tests__/
├── ComponentName.spec.ts
└── __snapshots__/
└── ComponentName.html # Always use .html suffix for component snapshots
Important: Component snapshot files must use .html suffix, NOT .snap suffix. This makes them easier to view and diff in code editors.
// Keep shared test data in tests/assets/
~/tests/aessst / models.json; // Models metadata and configs
~/tests/aessst / installedModels.json; // Installed models states
~/tests/aessst / requests.json; // Request/job data for testing
~/tests/aessst / inputs.json; // Input data for requests
~/tests/aessst / output.json; // Output data for results
~/tests/aessst / swagger.json; // API documentation data
// Import examples:
import testModels from '~/tests/assets/models.json';
import testInstalledModels from '~/tests/assets/installedModels.json';
import testRequests from '~/tests/assets/requests.json';
import testInputs from '~/tests/assets/inputs.json';
import testOutput from '~/tests/assets/output.json';
import testApi from '~/tests/assets/swagger.json';
mountSuspended from @nuxt/test-utils/runtime instead of mount from @vue/test-utilsmountSuspended properly handles Nuxt's async setup and SSR contextdata-test="element-name" for test element selectionawait async operations like mountSuspendedawait when triggering events that might cause async updatesCombine tests when possible to reduce redundancy and improve test suite performance:
Related assertions on the same component state
// ✅ GOOD - Combined related checks
it('renders correctly and matches snapshot', async () => {
const wrapper = await mountSuspended(ComponentName, {
props: { value: 'test' },
});
// Check basic rendering
expect(wrapper.find('[data-test="input"]').exists()).toBe(true);
expect(wrapper.find('[data-test="label"]').text()).toBe('Test Label');
// Snapshot captures full state
await expect(wrapper.html()).toMatchFileSnapshot(
'./__snapshots__/ComponentName.html',
);
});
// ❌ BAD - Separate tests for things that could be combined
it('renders input element', async () => {
const wrapper = await mountSuspended(ComponentName);
expect(wrapper.find('[data-test="input"]').exists()).toBe(true);
});
it('renders label', async () => {
const wrapper = await mountSuspended(ComponentName);
expect(wrapper.find('[data-test="label"]').text()).toBe('Test Label');
});
it('matches snapshot', async () => {
const wrapper = await mountSuspended(ComponentName);
await expect(wrapper.html()).toMatchFileSnapshot('./snapshot.html');
});
Sequential user interactions in a single flow
// ✅ GOOD - Test complete user flow
it('handles form submission flow', async () => {
const wrapper = await mountSuspended(FormComponent);
// Fill form
await wrapper.find('[data-test="name-input"]').setValue('John');
await wrapper
.find('[data-test="email-input"]')
.setValue('john@example.com');
// Submit
await wrapper.find('[data-test="submit-button"]').trigger('click');
await nextTick();
// Verify results
expect(mockSubmit).toHaveBeenCalledWith({
name: 'John',
email: 'john@example.com',
});
expect(wrapper.find('[data-test="success-message"]').exists()).toBe(true);
});
// ❌ BAD - Splitting a natural flow into separate tests
it('fills name field', async () => {
/* ... */
});
it('fills email field', async () => {
/* ... */
});
it('submits form', async () => {
/* ... */
});
it('shows success message', async () => {
/* ... */
});
Multiple checks on the same mock/spy
// ✅ GOOD - Check all aspects of the mock call together
it('calls API with correct parameters and handles response', async () => {
requestMock.mockReturnValueOnce({ data: { id: '123' } });
await store.createItem('test-name');
// Check call parameters
expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenCalledWith('/api/items', {
method: 'POST',
body: { name: 'test-name' },
});
// Check state update
expect(store.items).toHaveLength(1);
expect(store.items[0].id).toBe('123');
});
Keep tests separate when:
Testing different component states or configurations
// ✅ GOOD - Separate tests for distinct states
it('renders in loading state', async () => {
/* ... */
});
it('renders in error state', async () => {
/* ... */
});
it('renders in success state', async () => {
/* ... */
});
Testing different error conditions
// ✅ GOOD - Each error case is distinct
it('handles network error', async () => {
/* ... */
});
it('handles validation error', async () => {
/* ... */
});
it('handles timeout error', async () => {
/* ... */
});
Testing independent features
// ✅ GOOD - Unrelated features stay separate
it('opens modal when button clicked', async () => {
/* ... */
});
it('filters list when search term entered', async () => {
/* ... */
});
it('sorts items when header clicked', async () => {
/* ... */
});
When test names would become unclear
// ❌ BAD - Test name is too vague
it('does multiple things', async () => {
/* ... */
});
// ✅ GOOD - Clear, focused test names
it('validates email format', async () => {
/* ... */
});
it('shows error for duplicate email', async () => {
/* ... */
});
"renders form and validates input""handles complete checkout flow""submits form and shows success message"To check test coverage for specific files or the entire project:
# Run tests with coverage for a specific test file
TZ=UTC npx vitest run <test-name> --coverage.enabled --coverage.reporter=text
# Examples:
TZ=UTC npx vitest run useBenchmarkStore --coverage.enabled --coverage.reporter=text
TZ=UTC npx vitest run 2.results.global --coverage.enabled --coverage.reporter=text
# Run all tests with coverage
TZ=UTC pnpm test -- --coverage
# Run tests in watch mode with coverage
TZ=UTC pnpm test -- --coverage --watch
The coverage report shows four key metrics:
Example coverage output:
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
useBenchmarkStore | 96.63 | 92.85 | 94.44 | 96.63 | 193-199,451-461
2.results.global | 100 | 100 | 100 | 100 |
-------------------|---------|----------|---------|---------|-------------------
The project has the following coverage thresholds (defined in vitest.config.mts):
Aim to meet or exceed these thresholds when writing tests. For new code, strive for 100% coverage when practical.