name: test-style description: Conventions for writing tests in silkdown so they double as documentation and produce explicit, easy-to-pinpoint failure messages. Auto-trigger when creating or editing any file matching packages//test/**/.test.{ts,tsx} or e2e/**/*.spec.ts, when adding new vitest or Playwright tests, when reviewing existing tests for clarity, or when triaging a failing assertion that "doesn't tell you which thing broke". Also fires on phrases like "name this test", "test description", "split this test", "expect message", "assertion message", "expect.soft", "tests as documentation", "explicit failure", "vitest assert", "playwright test name", "what's a good test name".
Test style — silkdown
Tests in this repo are read at least as often as they are run. They serve as
the executable specification for the public API: a contributor (or AI
agent) should be able to grep for the behaviour they're about to change, read
the matching test names, and understand both what the system promises and
why. When a test fails in CI, the message must point at the specific
behaviour that broke — not at "expected true to be false".
Layered on top of tdd (which covers the loop) and react-best-practices
(component testing rules). When in doubt, prefer integration / "social unit"
tests that mount the real EditorView over isolated mocks.
Principles
- Test names describe behaviour, in full sentences —
<does X> when <Y>. Read-aloud check: a contributor with no context can predict the assertions from the title alone. → references/naming.md - One behaviour per test — split compound names (
"wraps and unwraps") into two tests. The cost is 5 lines; the gain is that CI tells you which half broke. → references/naming.md describeblocks name the behaviour area, not the file —describe("inline reveal — bold (StrongEmphasis)"), notdescribe("tests"). The describe name is half the test sentence. → references/naming.md- Every non-self-explanatory assertion gets a second-arg message —
expect(value, "what should be true").toBe(...). Required when multiple assertions in one test could fail ambiguously, when the literal compared value (null,true, a number) doesn't communicate intent, or when the diff would be a long DOM dump. → references/assertions.md assert(value, "msg")overif (!value) return;— theif-return pattern silently passes when fixtures are wrong. Use vitest'sassertto fail loudly with a fixture-pointing message and narrow the type for the rest of the test. → references/assertions.md- Don't drill through optional chains in the assertion target —
expect(link?.getAttribute("..."))hides the root cause. Assertlinkfirst, then drill in directly. → references/assertions.md - Name magic offsets — pull them from the doc string itself or hoist to
a named constant. A future reader sees
cursorOnLine2and knows the scenario. → references/structure.md - Always destroy mounted views and restore stubbed globals —
afterEachcallsview.destroy(), restoreswindow.open, runscleanup()for testing-library. Leaks corrupt the next test. → references/structure.md - e2e: assertion at every meaningful intermediate state — multi-press sequences must report which press broke things, not just the final state. → references/structure.md
- Don't mock
@codemirror/*or@silkdown/corecollaborators — repo tests are integration-style. Mocks are reserved for browser APIs (window.open) and external IO. → references/structure.md
Forbidden patterns (review-stoppers)
it("should ..."),it("works"),it("renders correctly"),it("test ...").- Compound names joined with
and/,:"wraps and unwraps". if (!fixture) return;— replaced withassert.expect(maybeNull?.something).toBe(...)— assertmaybeNullfirst.expect(true).toBe(true)placeholder assertions.- Mocking
@codemirror/*or@silkdown/corecollaborators.
Pre-commit checklist
- Title reads as a complete behavioural sentence.
- Title would still be accurate if the implementation were rewritten.
- Each assertion has either (a) a message, or (b) a self-explanatory failure value.
- No
if (!x) return;— replaced withassert. - No magic numeric offsets — named constants or computed from the doc.
- If the test mounts an
EditorView,afterEachcallsview.destroy(). - If the test stubs a global,
afterEachrestores it. - Two scenarios that diverge in one assertion are two
its, not one.
Examples
Four shapes cover ~95% of tests in this repo. See references/examples.md:
- Example A — pure-logic util test (URL policy / parser).
- Example B — integration test with a real
EditorView(decoration / widget). - Example C — React component test (view-stability invariant).
- Example D — Playwright e2e with intermediate-state assertions on a keyboard sequence.