# Pattern Testing Guide This is the canonical reference for testing Common Fabric patterns. ## Preconditions Before writing tests, verify the pattern exposes an interface that tests can exercise: - `pattern()` - actions typed as `Stream` where tests need to call `.send()` - bound handlers returned from the pattern when the behavior is externally triggered If those are missing, fix the pattern first. Tests cannot meaningfully drive the pattern without a testable output contract. ## Preferred Test Scope Write tests for: - state transitions that are awkward to verify by clicking - regressions that are easy to reintroduce - edge cases with real branching logic For Pattern Factory Build, prefer tests that cover the product contract rather than only the happy path: - first-run/default and sparse states - primary add, remove, edit, toggle, or submit flows - repeated actions and important state transitions - validation, empty, partial, or edge-case branches from the spec - helper or wrapper behavior that would otherwise only fail in a browser Avoid tests for: - code that is still only a sketch - behavior that is obvious and cheap to validate interactively - flows that are better validated in runtime or browser testing ## Test Command ```bash deno task cf test .test.tsx ``` If `cf test` fails, treat that as repair work. Preserve the failing command and relevant output, isolate the smallest failing action/assertion when useful, fix either the implementation or an invalid test contract, and rerun the test. A failing pattern test is not a valid done state unless a concrete external, tooling, or environment blocker prevents further repair. For non-obvious `cf test` failures, read `docs/development/debugging/README.md` before changing the test shape. Match the exact error to the matrix and follow the linked doc. For Cell, Writable, or reactive-value failures, also reread: - `docs/common/concepts/reactivity.md` - `docs/common/patterns/new-cells.md` ## Test File Shape The usual shape is: 1. instantiate the pattern under test 2. define actions that trigger output streams or bound handlers 3. define assertions as `computed(() => boolean)` 4. return the test sequence in order ```tsx // Shown at module scope. import { action, computed, pattern } from "commonfabric"; import Pattern from "./pattern.tsx"; export default pattern(() => { const instance = Pattern({ /* input */ }); const actionDoSomething = action(() => { instance.someAction.send(); }); const assertInitialState = computed(() => instance.someField === expectedValue); const assertAfterAction = computed(() => instance.someField === newValue); return { tests: [ { assertion: assertInitialState }, { action: actionDoSomething }, { assertion: assertAfterAction }, ], }; }); ``` ## Key Points - trigger actions with `.send()` when the output exposes streams - use direct property access for assertions rather than `.get()` unless the API truly requires writable access - keep scenario ordering readable; tests should tell the story of the state transition - test a sub-pattern before building the next dependent layer when that helps isolate failures ## Console Errors and Warnings Fail Tests `cf test` fails a test when anything is logged at error or warn level during the run phase, even if every assertion passes. Two channels are captured: `console.error`/`console.warn` calls from pattern code, and error/warn-level activity from the runtime's own loggers (reported by logger name and message key). A clean run is part of the contract — a passing test that logs errors hides real failures (this is how a production CFC commit-rejection shipped behind green tests). If a test intentionally provokes errors or warnings, opt out explicitly on the returned descriptor — each flag covers only its own level: ```tsx // Shown inside a pattern body. return { tests: [/* ... */], allowConsoleErrors: true, // expected console/logger errors don't fail allowConsoleWarnings: true, // expected console/logger warnings don't fail }; ``` In multi-user tests the flags are per participant: one participant opting out does not mask another participant's errors. (The same applies to the pre-existing `allowRuntimeErrors` flag for scheduler-level errors.) ## Multi-User Tests A single-runtime test cannot exercise `PerUser`/`PerSession` scoping or cross-client propagation — one runtime is one user and one session. For patterns with multi-user behavior, export a `multiUserTest` descriptor as the default export instead of a single test pattern. `cf test` then runs each participant pattern in its own isolated runtime (own identity, own realm) against one shared space on an in-process storage server. ```tsx // Shown for illustration only. import { action, computed, multiUserTest, pattern } from "commonfabric"; import Chat, { type ChatOutput } from "./pattern.tsx"; interface Setup { chat: ChatOutput; } // Instantiates the shared state ONCE; every participant runtime runs this // same instance (like every browser tab does) and receives its result as // the `setup` input. export const setup = pattern(() => ({ chat: Chat({}) })); export const alice = pattern<{ setup: Setup }>(({ setup }) => { const save = action(() => setup.chat.saveProfile.send()); const sees_bob = computed(() => /* ... */); return { tests: [ { action: save }, { label: "alice-saved" }, // announce a marker { await: "bob-saved" }, // park until bob announces { assertion: sees_bob }, ], }; }); export const bob = pattern<{ setup: Setup }>(({ setup }) => { /* ... */ }); export default multiUserTest({ setup, participants: { alice, bob } }); ``` Key points: - A participant's steps run in order; cross-participant ordering happens ONLY at `{ label: "name" }` / `{ await: "name" }` markers. If every remaining participant is parked on an unannounced marker, the test fails with a deadlock report. - Each participant gets its own identity. Use `{ pattern: aliceTab2, user: "alice" }` for a second session of an existing user (PerUser state shared, PerSession state isolated). - Assertions retry (with settling) until the step timeout, since asserted state may still be propagating from another runtime — don't assert "other user does NOT see X yet" right after the other user acted; assert stable invariants instead. - Pattern outputs a participant asserts on must be computed snapshots that always yield a REAL, STABLE value. In a runtime that didn't write the underlying scoped cell, the cell reads as `undefined` — and a computed that returns `undefined` (or a fresh `[]` per recompute) is indistinguishable from "not yet computed" for cross-runtime readers, so the assertion never settles. Normalize inside the computed (`trimmedName(name.get())`, `cell.get() ?? EMPTY_LIST` with a module-level constant). - Read another runtime's arrays with INLINE literal indexing in the assertion computed (`users?.[0]?.name === "Alice"`). `.map()`, loop-variable indexing, and module-level helper calls over the array resolve in the runtime that wrote it but NOT cross-runtime before a local write. - A participant cannot read their own never-written `PerUser` array (e.g. an empty rack before joining); assert pre-join isolation via normalized primitives (`myName === ""`) instead. - The example to copy: `packages/patterns/cfc-group-chat-demo/multi-user.test.tsx`; for the output-snapshot and inline-read idioms see `packages/patterns/scrabble/multi-user.test.tsx` and `packages/patterns/lunch-poll/multi-user.test.tsx`. The scope model background: `docs/common/patterns/multi-user-patterns.md` and `docs/development/debugging/gotchas/scoped-cell-pitfalls.md`. ## Testing Time and Randomness If a pattern uses `safeDateNow()` or `nonPrivateRandom()`, keep the assertions deterministic: - prefer asserting that a value was set, changed, or has the expected shape - avoid calling `safeDateNow()`, `nonPrivateRandom()`, `Date.now()`, or `Math.random()` inside a test `computed()` assertion - if you need an exact value, capture it in the action under test and assert against the captured result rather than recomputing it in the assertion ## Done When - the test file exists beside the pattern or in the expected local test layout - the tests pass - the test report explains what was covered and what was intentionally omitted - the pattern still compiles after any interface changes made to support testing - the pattern is ready for the next dependent sub-pattern or release step