cratis-application-react-specifications · git:20261012.b518110 · 2026-10-12 · sha256 ffe1a381f6eb9eac
cratis-application-react-specifications git:20261012.b518110A
Immutable. This exact content never changes and is served at /api/v1/blob/ffe1a381f6eb9eac.
---
name: cratis-application-react-specifications
description: Write specifications for the React and TypeScript surface of a Cratis application slice — view models, helpers, command orchestration, and narrow component behavior — using Vitest with Mocha-style describe/it, Sinon, and the Chai should interface. Use when adding or changing frontend behavior in an application that consumes Cratis. Do not use for backend scenarios or for specifications inside a Cratis framework package.
license: MIT
metadata:
cratis-hint-paths: "**/for_*/**/*.ts **/for_*/**/*.tsx"
---
# Cratis application React specifications
Frontend specifications are specifications, not tests. The `for_ / when_ / and_`
shape is deliberately the same BDD language as the backend, so one subject, one
context, clear setup, and small assertions read as sentences.
This is the **application** frontend peer of the backend slice specification
skill. Specifications for a Cratis framework package (`@cratis/*`) follow that
repository's own TypeScript specification conventions instead.
## Stack
- **Vitest** for the runner, with Mocha-style `describe` / `beforeEach` / `it`.
- **Sinon** for stubs and fakes (`sinon.stub()`, `sinon.createStubInstance()`).
- **Chai** with the **`.should` fluent interface** — never `expect()`.
`result.should.equal(expected)` reads as a sentence.
## Placement and naming
Specifications live next to the unit they specify, in snake_case so the path
reads as a scenario sentence:
```
<Slice>/
<Subject>.ts
for_<Subject>/
when_<context>/
and_<extra_context>.ts
```
| Shape | Use for |
| --- | --- |
| `for_<Subject>/when_<context>/and_<extra>.ts` | the default — view models, branching helpers, component specs |
| `for_<Subject>/when_<context>.ts` | a tiny pure helper with no useful extra context |
`describe('when ...')` names the scenario, not the class. `it('should ...')`
uses spaces, not underscores, and asserts one observable outcome. Do not add
`.test.ts` or `.spec.ts` files unless the project's configuration changes
deliberately.
## What to specify
Behavior that can regress without TypeScript catching it:
- **View models** — the primary target. State transitions, computed getters,
filtering, sorting, selection, wizard steps, validation state, and
command-value derivation.
- **Command orchestration outside a command dialog** — the unauthorized,
invalid, exception, and success branches.
- **Pure helpers** — parsing, formatting, grouping, boundary cases.
- **Component rendering** only when the markup, a disabled state, or wrapper
behavior is genuinely the point.
Do **not** specify generated Cratis proxies, Cratis Components or framework internals,
CSS pixel-perfection, snapshots, or trivial presentational pass-throughs.
## View-model specification — the default shape
A view model is a plain class: constructible directly, with no React hooks, no
direct `window`, `localStorage`, timer, or network access (inject the Cratis
abstraction instead), and derived values as 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));
});
```
`beforeEach` does the arrange and act once for the context; each `it` asserts
one outcome. Pass small typed fakes or `sinon.stub()` instances for
dependencies, and do not build a shared harness until a second specification
actually reuses it.
A view model that cannot be constructed without React is doing too much or
depending on the wrong abstraction — that is a design signal, not a testing
problem.
## Command-result branches
When a command runs outside a command dialog, each granular flag is a branch
worth specifying: `isAuthorized`, `isValid` with its `validationResults`,
`hasExceptions`, and the success path. Stub the command and return a result
object per branch rather than reaching for the network.
## Component specification — when rendering is the point
The environment is `node`. For behavior that does not need browser events,
render server-side markup and mock only the external component boundary:
```ts
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, beforeEach, it, vi } from 'vitest';
vi.mock('@cratis/components/Dialogs', () => ({
Dialog: (props: { buttons?: React.ReactNode; children?: React.ReactNode }) =>
React.createElement('div', null, props.children, props.buttons),
}));
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'));
});
```
Prefer typed mock props over `any`. Add jsdom or Testing Library only when the
behavior truly requires DOM events — do not make it the default. Avoid
snapshots; they hide behavior and make refactors noisy.
Cratis Components mount inside `CratisComponentsProvider`, so a specification
that renders one for real must provide it (enable `toaster` only when asserting
toasts). In jsdom, provide a `ResizeObserver` stub for any Components area that
observes size, including observable tables, Filter and toolbars. Source-checked
at Components v4.26.4 (`Source/**`); this is not a split-pane-only requirement.
## Async and time
Use `vi.useFakeTimers()` for time-dependent behavior and restore with
`vi.useRealTimers()` in `afterEach`. Never depend on the real current date,
random values, the network, or browser storage. For suspense-query
specifications, clear the Arc query caches in teardown with
`clearSuspenseQueryCache()` and `clearSuspenseObservableQueryCache()` from
`@cratis/arc.react/queries`.
## Stories are not specifications
Storybook covers visual states, documentation, and manual verification; Vitest
covers behavior. A component with non-trivial logic usually needs both. A story
file is colocated with its component, uses
`satisfies Meta<typeof Component>` rather than a type annotation, and carries
`tags: ['autodocs']`. Write stories for reusable components — shared primitives
and slice-internal components reused across slices — not for live slice pages
wired to Arc query and command hooks; exercise those in the running app.
An interaction (`play:`) function belongs on stateful components only, and it is
documentation and QA rather than the enforced gate. The enforced behavioral gate
is the Vitest specification.
## The engineering bar these specifications serve
- No placeholder or dead UI ships for behavior the code actually implements.
- Components stay small and single-responsibility; a `// Section` comment inside
a component means that section is its own component.
- State logic lives in a view model or a tested state module, not in the
component — extract as soon as there are three or more `useState` calls, any
state-synchronizing `useEffect`, or derived values.
- Reuse an existing Cratis Components wrapper or shared primitive before adding
a new component.
- `unknown` or a real type, never `any`.
- After editing a `.tsx`, read past its final closing brace to confirm no stale
return block or unreachable code remains.
## Gate
Run the project's frontend test gate (typically `yarn test`) plus lint and the
TypeScript build. Specifications complement lint and build; they do not replace
them.
## Route near misses
- An accepted `.play` model under the model root covers the behavior, or the
repository is opted in (the root holds a committed `.play` file (`git ls-tree -r --name-only HEAD` lists a `.play` file there, narrowed to `-- <root>` when a root is configured), or the project set
`mcpServers.screenplay.root` in `.cratis/ai.json`; an empty directory, install
output, an uncommitted `.play` draft or a `.play` file outside the root does not count; master definition:
`cratis-screenplay-modeling-lifecycle`): change the model first with
`cratis-screenplay-event-modeling`. If the Screenplay skills are not installed,
say so and do not author `.play` from memory.
Edit code here only for infrastructure, clients, adapters, Screenplay code
attachments, or gap-fill scope (`cratis-screenplay-render-and-gap-fill`);
never edit Stage-managed output.
## Verify
- Files sit in `for_<Subject>/when_<context>/` beside the unit, in snake_case.
- `describe` names the scenario; `it` descriptions start with "should" and use
spaces.
- Assertions use the Chai `.should` interface, never `expect()`.
- Each `it` asserts one observable outcome.
- View models are constructed directly, with stubs for dependencies and no
React involved.
- No specification covers a generated proxy, a framework internal, or a
snapshot.
- Fake timers are restored in `afterEach` and query caches are cleared for
suspense specifications.
- Lint, the frontend test gate, and the TypeScript build all pass.