Agent Skills › YosemiteCrew/Yosemite-Crew › frontend-testing

frontend-testing

GitHub

用于前端应用测试编写与修复的规范,涵盖Jest和RTL约定、95%覆盖率强制要求及多层级测试策略。

.agents/skills/frontend-testing/SKILL.md YosemiteCrew/Yosemite-Crew

Trigger Scenarios

编写或修复 apps/frontend 中的测试文件 执行前端测试任务

Install

npx skills add YosemiteCrew/Yosemite-Crew --skill frontend-testing -g -y
More Options

Non-standard path

npx skills add https://github.com/YosemiteCrew/Yosemite-Crew/tree/main/.agents/skills/frontend-testing -g -y

Use without installing

npx skills use YosemiteCrew/Yosemite-Crew@frontend-testing

指定 Agent (Claude Code)

npx skills add YosemiteCrew/Yosemite-Crew --skill frontend-testing -a claude-code -g -y

安装 repo 全部 skill

npx skills add YosemiteCrew/Yosemite-Crew --all -g -y

预览 repo 内 skill

npx skills add YosemiteCrew/Yosemite-Crew --list

SKILL.md

Frontmatter
{
    "name": "frontend-testing",
    "description": "Use when writing or fixing tests in apps\/frontend. Covers Jest and React Testing Library conventions, targeted test runs, the coverage mandate, Zustand mocking, and common pitfalls."
}

Frontend Testing — Yosemite Crew

Description

Use this skill when writing or fixing tests in apps/frontend. Covers Jest + React Testing Library conventions, targeting rules, and common pitfalls in this codebase.

TRIGGER: any task involving test files in apps/frontend, or when asked to write/fix/run frontend tests.


Coverage Mandate — Non-Negotiable

Target: ≥ 95% Statements, Branches, Functions, Lines across apps/frontend. Every change must move coverage upward, never downward.

Rules that apply to every task — add, modify, remove

  1. Any file you touch must finish with equal or higher coverage than you found it. Run the targeted test and confirm before handoff.
  2. Any file you create must hit ≥ 90% Statements, Branches, Functions on first commit. New code with no tests is a blocker — do not declare the task done.
  3. When you delete code, delete the corresponding test code too. Dead test scaffolding that no longer maps to real behaviour inflates noise and hides real gaps.
  4. When you modify behaviour (rename, refactor, add a branch, change a conditional), update every existing test that covers the changed path AND add new cases for new branches.
  5. Snapshot tests count but do not substitute for behavioural assertions. A snapshot alone does not satisfy coverage for a branch. Every logical branch needs at least one assertion that validates the outcome.

Test types required — use all of them, not just one

Layer Tool When required
Unit Jest Every service, store, hook, utility, helper
Component React Testing Library (RTL) Every UI component — render + interaction + conditional rendering
Snapshot Jest toMatchSnapshot / toMatchInlineSnapshot Stable UI layouts — complement behavioural tests, never replace them
E2E Playwright (e2e/) Auth flows, booking, checkout, payment, any critical user journey

All four layers must grow together. Do not add 20 RTL tests while leaving Playwright untouched for a critical flow, and vice versa.

Coverage enforcement workflow

# After every change, run coverage for the touched file(s):
pnpm --filter frontend run test -- --testPathPatterns="<YourFile>" --coverage --collectCoverageFrom="src/app/path/to/YourFile.tsx"

# Check the output — if Statements/Branches/Functions dropped vs what you started with, add tests before declaring done.

If adding a test for a previously-uncovered branch, note it in the COMMIT CHECKPOINT message (e.g. test(frontend): improve branch coverage for CompanionHistoryPage loading state).


New Code = New Tests (Mandatory)

Every new module, service, hook, store, utility, or component added to apps/frontend must ship with tests in the same batch. No exceptions.

What you add What you must also add
Service function / API call Jest unit: success + all error branches (axios + non-axios)
Zustand store Jest: every action, selector, guard, and edge case
Custom hook renderHook covering all return values and state branches
Utility / lib function Jest unit with full branch coverage
UI component RTL render + at least one user interaction test
E2E-critical flow (auth, booking, checkout, payment) Playwright test in e2e/

Coverage bar for any new file you author: Statements ≥ 90%, Branches ≥ 90%, Functions ≥ 90%.

Do not leave an existing file in a worse coverage state than you found it. If you touch a file, hold or improve its coverage.


Mandatory Checks — Run in This Order After Every Change

Run all three every time you touch apps/frontend. Never skip any step.

# 1. Type check — run from apps/frontend/
npx tsc --noemit

# 2. Lint — run from repo root
pnpm --filter frontend run lint

# 3. Prefer targeted tests for the files you modified; full Jest runs are allowed if the user explicitly asks or if you are validating shared test infrastructure
pnpm --filter frontend run test -- --testPathPatterns="<ModifiedComponentName>"

# Examples
pnpm --filter frontend run test -- --testPathPatterns="CompanionCard"
pnpm --filter frontend run test -- --testPathPatterns="Availability"
pnpm --filter frontend run test -- --testPathPatterns="__tests__/features/billing"

Full suite is discouraged by default. Use targeted runs for normal development, but a full Jest run is allowed if the user explicitly asks, if you are triaging repo-wide breakage, or if you changed shared test infrastructure. Playwright and accessibility runs are allowed whenever they are relevant.

When modifying an existing file, always check whether a test file already exists for it (look in src/app/__tests__/ mirroring the source path). If it does, run it and fix any failures your change introduced before declaring the task done. A change is not complete if it breaks existing tests.


Stack

  • Jest 30 + React Testing Library (@testing-library/react, @testing-library/user-event). Jest 30 renamed the targeting flag to --testPathPatterns (plural) - the old singular form errors.
  • Playwright for E2E (separate from unit/integration tests)
  • Test files: src/app/__tests__/
  • Jest config: apps/frontend/jest.config.ts
  • Mocks: src/app/jest.mocks/

Rules

DOM Nesting

jest.spyOn(console, 'error') checks are active — DOM nesting warnings are treated as test failures.

  • Never render <button> inside <button>.
  • Mock react-icons as <span> not <button>:
// jest.mocks or inline
jest.mock('react-icons/fa', () => ({
  FaUser: () => <span data-testid="icon-user" />,
  FaPlus: () => <span data-testid="icon-plus" />,
}));

Async State

// Always wrap async state updates
await act(async () => {
  userEvent.click(button);
});

Zustand Stores

Reset store state between tests to avoid leakage:

beforeEach(() => {
  useAuthStore.setState({ user: null, isAuthenticated: false });
});

Mocking stores that use .getState() outside React

When a hook calls useXxxStore.getState() directly (e.g. to read status without subscribing), jest.mock alone produces a plain function with no getState method and the test will throw TypeError: useXxxStore.getState is not a function.

Two patterns depending on how the store is mocked:

  1. jest.mock('@/app/stores/xxxStore') (auto-mock) — attach getState in beforeEach:
const mockGetState = jest.fn();

beforeEach(() => {
  (useXxxStore as unknown as jest.Mock).mockImplementation((selector) => selector(mockState));
  mockGetState.mockReturnValue(mockState);
  (useXxxStore as unknown as jest.Mock & { getState: jest.Mock }).getState = mockGetState;
});
  1. jest.mock('...', () => ({ useXxxStore: jest.fn() })) (explicit factory) — include getState in the factory and wire it in beforeEach:
jest.mock('@/app/stores/xxxStore', () => ({
  useXxxStore: Object.assign(jest.fn(), { getState: jest.fn() }),
}));

const mockGetState = jest.fn();

beforeEach(() => {
  (useXxxStore as unknown as jest.Mock).mockImplementation((selector) => selector(mockState));
  mockGetState.mockReturnValue(mockState);
  (useXxxStore as unknown as jest.Mock & { getState: jest.Mock }).getState = mockGetState;
});

Always update mockGetState.mockReturnValue(mockState) whenever mockState changes mid-test so that getState() stays in sync with the selector mock.

API Mocking

Use jest.spyOn on axios or mock at the module level. Never make real HTTP calls in tests.

import axios from 'axios';
jest.spyOn(axios, 'get').mockResolvedValue({ data: mockData });

Query Priority (Testing Library)

In order of preference:

  1. getByRole — semantic, accessible
  2. getByLabelText — forms
  3. getByText — when role isn't meaningful
  4. getByTestId — last resort, only when no other selector works

File Structure

src/app/__tests__/
  features/           ← feature-level tests
  components/         ← component unit tests
  hooks/              ← custom hook tests
  utils/              ← utility function tests

Test file naming: ComponentName.test.tsx mirrors the source file name.


Gotchas

  • Never use screen.getByDisplayValue for controlled inputs — use getByRole('textbox') + check value.
  • userEvent needs await in v14+ — always await userEvent.click(el).
  • If a test imports from @/app/ui, make sure the mock is at module level, not inside describe.
  • Playwright tests live in e2e/ and run separately — don't confuse them with Jest tests.
  • If --testPathPatterns matches multiple files unintentionally, be more specific with the path.

Pitfalls Discovered During Coverage Work (2025–2026)

require() is forbidden — use top-level imports instead

ESLint rule @typescript-eslint/no-require-imports blocks require() inside test bodies. Never do:

// ❌ forbidden
const { fromFormRequestDTO } = require('@yosemite-crew/types');

Instead, import at the top of the file and cast:

// ✅ correct
import { fromFormRequestDTO } from '@yosemite-crew/types';
// ...
(fromFormRequestDTO as jest.Mock).mockImplementationOnce(() => {
  throw new Error('invalid');
});

jest.resetAllMocks() wipes factory mock return values

If you use jest.resetAllMocks() in beforeEach, any mock initialized with .mockReturnValue() in a jest.mock() factory is reset to undefined. Re-initialize all mock return values inside beforeEach after resetAllMocks():

beforeEach(() => {
  jest.resetAllMocks();
  // Must re-set these — factory defaults are gone after resetAllMocks
  (canTransitionAppointmentStatus as jest.Mock).mockReturnValue(true);
  (useAuthStore.getState as jest.Mock).mockReturnValue({ user: mockUser, attributes: {} });
});

axios.isAxiosError mock — use jest.mock("axios", ...) not jest.spyOn

jest.spyOn on axios.isAxiosError doesn't reliably work because the service imports axios at module load time. Use:

jest.mock('axios', () => ({
  create: jest.fn(() => ({
    interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
  })),
  isAxiosError: jest.fn(),
}));
// Then per test:
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
// After test:
(axios.isAxiosError as jest.Mock).mockReset();

Read-only DOM properties require Object.defineProperty

Object.assign(el, { scrollTop: 0 }) throws because scrollTop is a getter-only property on HTMLElement. Use:

Object.defineProperty(el, 'scrollTop', { value: 0, writable: true, configurable: true });

Module-level singletons break cross-test isolation with jest.resetModules()

Services that maintain module-level singletons (e.g. connectionPromise, chatClient) can't easily test "connection in progress" state when jest.resetModules() resets the module between each test. Drop those test scenarios or use a single beforeAll import for that specific describe block.

performAppointmentAction requires a valid lead.id for accept action

When testing acceptAppointment or changeAppointmentStatus → UPCOMING, the appointment must have a non-empty lead.id or the service throws "Cannot accept appointment without a valid lead." Always include lead: { id: 'vet-1', name: 'Dr Vet' } in those test fixtures.

canTransitionAppointmentStatus from @/app/lib/appointments

This function is imported by appointmentService.ts from @/app/lib/appointments (not from a utils sub-path). Mock it as:

jest.mock('@/app/lib/appointments', () => ({
  canTransitionAppointmentStatus: jest.fn(),
  getInvalidAppointmentStatusTransitionMessage: jest.fn().mockReturnValue('Invalid transition'),
}));

And re-initialize in beforeEach after resetAllMocks():

const { canTransitionAppointmentStatus } = jest.requireMock('@/app/lib/appointments');
(canTransitionAppointmentStatus as jest.Mock).mockReturnValue(true);

Auth and team stores need re-initialization when using resetAllMocks()

useAuthStore and useTeamStore are imported by appointmentService. If your test file uses jest.resetAllMocks(), you must re-seed these in beforeEach:

const { useAuthStore } = jest.requireMock('@/app/stores/authStore');
(useAuthStore.getState as jest.Mock).mockReturnValue({
  user: { getUsername: jest.fn().mockReturnValue('user-1') },
  attributes: {},
});

getValidSession branch logic in authStore

isSessionFresh checks session.isValid() first, then reads payload.exp. Testing branches:

  • Pass a session with isValid: () => true and exp far in the future → returns cached session
  • Pass null session → triggers refresh
  • Pass session where decodePayload throws → falls back to session.isValid() return value
  • forceRefresh: true + null refreshed session → returns null without calling checkSession

Version History

  • 7f92970 Current 2026-08-29 01:14

    根据提交日志更新文档以反映 Jest 30 支持,清理过时的设计令牌声明及相关错误描述。

  • 3726483 2026-08-20 13:37

Same Skill Collection

.agents/skills/agent-loop/SKILL.md
.agents/skills/backend-patterns/SKILL.md
.agents/skills/code-review/SKILL.md
.agents/skills/desktop-sonar/SKILL.md
.agents/skills/frontend-design/SKILL.md
.agents/skills/frontend-sonar/SKILL.md
.agents/skills/mobile-patterns/SKILL.md
.agents/skills/monorepo-ops/SKILL.md
.agents/skills/react-doctor/SKILL.md
.agents/skills/yosemite-client-communications/SKILL.md
.agents/skills/yosemite-data-migration-audit/SKILL.md
.agents/skills/yosemite-inventory-planning/SKILL.md
.agents/skills/yosemite-practice-workflow-audit/SKILL.md
.agents/skills/yosemite-staff-onboarding/SKILL.md
.agents/skills/yosemite-vet-software-buyer/SKILL.md
.agents/skills/yosemite-vet-visit-prep/SKILL.md
.claude/skills/agent-loop/SKILL.md
.claude/skills/backend-patterns/SKILL.md
.claude/skills/code-review/SKILL.md
.claude/skills/desktop-sonar/SKILL.md
.claude/skills/frontend-design/SKILL.md
.claude/skills/frontend-sonar/SKILL.md
.claude/skills/frontend-testing/SKILL.md
.claude/skills/mobile-patterns/SKILL.md
.claude/skills/monorepo-ops/SKILL.md
.claude/skills/react-doctor/SKILL.md
.claude/skills/yosemite-client-communications/SKILL.md
.claude/skills/yosemite-data-migration-audit/SKILL.md
.claude/skills/yosemite-inventory-planning/SKILL.md
.claude/skills/yosemite-practice-workflow-audit/SKILL.md
.claude/skills/yosemite-staff-onboarding/SKILL.md
.claude/skills/yosemite-vet-software-buyer/SKILL.md
.claude/skills/yosemite-vet-visit-prep/SKILL.md

Metadata

Files
0
Version
6932903
Hash
4f83096b
Indexed
2026-08-20 13:37

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-28 23:38
浙ICP备14020137号-1