--- name: write-specs-frontend description: Use this skill to write specs for the React/TypeScript surface of a Cratis APPLICATION slice — view models, helpers, and component behavior (Vitest + Mocha-style describe/it + Sinon + Chai .should). The frontend peer to write-specs (backend). For framework @cratis/* package specs use cratis-specs-typescript instead. --- Write specs for a slice's React/TypeScript surface — view models, helpers, and narrow component behavior. Full conventions live in [frontend-testing.md](https://github.com/Cratis/AI/blob/main/.ai/rules/frontend-testing.md); this skill is the **application** frontend peer to **write-specs** (backend). For **framework** `@cratis/*` package specs (the `given()` helper), use **cratis-specs-typescript** instead. ## Placement & naming Specs live next to the unit, snake_case so the path reads as a sentence: ``` / .ts for_/ when_/ and_.ts ``` `describe('when ...')` names the scenario, not the class; `it('should ...')` uses **spaces**; each `it` asserts one observable outcome. ## Stack Vitest (`describe`/`beforeEach`/`it`) · SinonJS (`sinon.stub()` / `createStubInstance`) · Chai **`.should`** fluent — never `expect()`. ## What to cover - **View models** (the primary target): state transitions, computed getters, filtering/sorting/selection, wizard steps, validation state, command-value derivation. - **Command orchestration outside `CommandDialog`:** unauthorized / invalid / exception / success handling. - **Pure helpers:** parsing, formatting, grouping, boundary cases. - **Component rendering** only when markup, disabled state, or wrapper behavior is the point. **Don't spec:** generated Cratis proxies, PrimeReact/framework internals, CSS pixel-perfection, snapshots, or trivial presentational pass-throughs. ## View-model spec (default) View models are plain classes constructible directly — no React hooks; no direct `window`/`localStorage`/timers/network (inject the abstraction); derived values are getters. ```ts import { describe, beforeEach, it } from 'vitest'; import { CandidatesViewModel } from '../../CandidatesViewModel'; describe('when filtering by search text', () => { let viewModel: CandidatesViewModel; beforeEach(() => { viewModel = new CandidatesViewModel(); viewModel.setSearch('senior'); }); it('should keep only matching candidates', () => viewModel.filteredItems.should.have.lengthOf(1)); }); ``` Pass small typed fakes / `sinon.stub()` for dependencies; don't build a broad harness until a second spec reuses it. ## Component spec (when rendering is the point) Environment is `node`; render server-side markup and mock only the external component boundary: ```ts import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { vi } from 'vitest'; vi.mock('primereact/dialog', () => ({ Dialog: (p: { footer?: React.ReactNode; children?: React.ReactNode }) => React.createElement('div', null, p.footer, p.children), })); describe('when rendered while busy', () => { let html: string; beforeEach(() => { html = renderToStaticMarkup(React.createElement(MyDialog, { isBusy: true })); }); it('should disable the confirm button', () => html.should.include('disabled')); }); ``` Add jsdom / Testing Library only when DOM events are truly required; avoid snapshots. ## Async & time `vi.useFakeTimers()` / `vi.useRealTimers()` in `afterEach`; never depend on the real date, random values, network, or storage. Clear the Arc query caches in teardown for suspense-query tests. ## Gate Run `yarn test` (or the project's frontend test gate) plus `yarn lint` and `npx tsc -b` — specs complement lint and build, they don't replace them. ## See also - [frontend-testing.md](https://github.com/Cratis/AI/blob/main/.ai/rules/frontend-testing.md) — the full conventions this applies. - [react.md](https://github.com/Cratis/AI/blob/main/.ai/rules/react.md) — MVVM, view-model testability, Arc hooks. - `write-specs` — the backend peer; `cratis-specs-typescript` — framework `@cratis/*` package specs.