# Testing

> Skill: testing

- Skill: `lofcz/testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lofcz/testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lofcz/testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lofcz (https://skillmd.com/u/lofcz)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lofcz/testing

---


## Testing Goal

Push the suite toward three layers only:

- Pure unit tests for deterministic logic.
- Thin editor or plugin contract tests for real Plate or Slate wiring.
- Golden input or output tests for serializer and parser behavior.

Hard constraints:

- Bun-first and speed-first. `bun run test` is the default iterative workflow.
- `pnpm test:all` is the full end-of-task and CI run. Do not use it as the default inner-loop command.
- Keep the default iterative suite fast.
- No browser or e2e coverage in this program.
- Coverage is hotspot telemetry, not a vanity target. Do not chase repo-wide numbers blindly.
- Use coverage after each phase only to choose the next hotspot.
- No one-smoke-test-per-package sweep.
- Do not add broad smoke coverage for thin wrapper packages.

## Coverage Strategy

- Coverage is for regression detection during breaking changes and rearchitecture, not for winning a percentage contest.
- Rank files from fresh `lcov`. Trust file order more than package totals.
- Work in passes:
  1. high-value contract pass: do every honest file with score `>= 6`
  2. medium-value follow-up: rerun coverage, then do worthwhile `>= 5` files while skipping crumbs, wrappers, and sludge
  3. architecture-safety pass: stop following coverage blindly and harden the contracts you most refuse to break
- File-first beats package sweeps once the obvious packages are already covered.
- Good architecture-safety targets:
  - plugin resolution and composition
  - normalization contracts
  - parser and serializer behavior
  - structural transforms and merge helpers
  - history, diff, and change-tracking behavior
  - public editor invariants
- `/react` is not permanently excluded. Exclude it only when the current pass explicitly says so.
- React work should wait until non-React seams are exhausted only when that is the active phase goal, not because the skill hardcodes it forever.
- Stop when the remaining misses are mostly:
  - thin wrappers
  - DOM-only or provider-only seams
  - giant low-ROI sludge files
  - tiny uncovered crumbs
  - code likely to be deleted or rewritten soon

## Core Rules

- Assert public behavior through editor APIs, plugin APIs, hooks, transforms, or rendered output. Do not assert private state, call order, or implementation detail when public behavior already proves the contract.
- Prefer file-ranked batches over package sweeps. Package sweeps are for early broad passes, not the endgame.
- Do not use coverage to justify testing files with no meaningful contract.
- Bun globals come from `tooling/config/global.d.ts`. Do not import `describe`, `it`, `expect`, `mock`, `spyOn`, or other globals from `bun:test`.
- Use `*.spec.ts[x]` for the fast lane and `*.slow.ts[x]` for the slow lane.
- Keep helpers package-local. Never import a helper from another spec file.
- No spec should import another spec.
- Put compile-only type contracts in `type-tests/`, not mixed into runtime specs.
- Titles should describe behavior semantically, not echo raw option names.
- Prefer explicit assertions over snapshots by default.
- Delete skipped tests, commented-out tests, and dead placeholder cases. Do not preserve wishful thinking.
- No fake smoke tests.
- No app-registry imports in package tests.
- Do not add package `devDependencies` just to support cross-package or app-shaped test setups. If a test needs other package kits, app aliases, or multi-package wiring, move it to `apps/www/src/__tests__/package-integration`.
- When adding tests, inspect fast-suite outliers with `bun run test:profile`. `bun run test:slowest` is the hard gate. If a fast-suite spec crosses the thresholds in `tooling/config/test-suites.mjs`, rename it to `*.slow.ts[x]`.
- Do not trust a fast laptop. Treat the local warning zone as real debt before CI proves you wrong: `60ms/test` or `120ms/file` is already a move candidate, especially for React-heavy specs.
- For borderline cases, run `pnpm test:slowest -- --top 25 --rerun-each 3` and move repeat offenders before they drift over the CI line.
- Treat known third-party resource logs and serializer fallback warnings as test noise. Suppress them narrowly in the shared Bun setup at `tooling/config/bunTestSetup.ts`, not by changing runtime code or sprinkling per-spec console mocks.

## Seam Selection

- Use `createEditor` from `@platejs/slate` for pure Slate query, transform, interface, and history contracts.
- Use `createSlateEditor` for non-React plugin or editor wiring:
  - plugin option stores
  - selector extension
  - pure plugin API composition
  - pure transform composition
  - parser and deserializer contracts
  - HTML `insertData`
  - DnD-style contracts
- Use `createPlateEditor` only when the contract is genuinely Plate-specific.
- Use rendered React tests only when the contract is genuinely React-specific: hooks, providers, stores, DOM behavior, or rerender semantics.
- Remaining `createPlateEditor` usage is a reviewed allowlist, not a future cleanup queue.

## File Organization

- File-scoped specs live beside the implementation.
- Keep `__tests__/` only for:
  - package-local helpers
  - fixture banks
  - intentionally split multi-file behavior suites
  - intentionally kept integration suites
- Move lone file-scoped specs out of `__tests__/`.
- Collapse tiny split suites into one adjacent table-driven file when the only variation is fixture shape.
- Action helpers may create the editor and perform the transform, but assertions stay in the `it()` body.
- Rule-action helpers should return the editor and other setup results, not assert internally.
- For composition-heavy suites, extract focused helpers before adding more inline setup. Small helpers like `getSortedKeys(...)` and `createStoreEditor(...)` beat repeated editor construction sludge.

## Fixtures And Assertions

- Use JSX hyperscript only when tree shape or selection shape is the contract.
- Use plain object fixtures for option, state, and pure helper tests.
- Use a real editor object only when editor-root semantics matter. `NodeApi` and `ElementApi` do not treat plain `{ children: [...] }` objects the same way as real editors.
- Keep inputs and outputs small.
- Use `it.each` for small behavior matrices.
- In this Bun + Testing Library setup, prefer render-returned queries over `screen`.
- Snapshots are allowed only when serialized text, AST, or similar output is the contract and inline assertions would be worse.
- Whitespace-sensitive serializer outputs should prefer direct `toBe(...)` string assertions.
- Avoid `toHaveStyle` here. Use direct style-property assertions instead.
- After broad title renames on snapshot-backed suites, delete and regenerate the snapshot file. `bun test -u` updates and adds keys, but does not reliably prune dead ones.

## Cleanup Heuristics

- Score files before cleanup waves instead of skimming randomly.
- Use fresh `lcov` after each pass. Do not keep working from a stale hotspot map.
- Rewrite large hotspot specs before chasing broad title debt. Bigger signal first.
- Scan title debt across:
  - plain string titles
  - `it.each(...)` format strings
  - `String.raw` titles
  - snapshot keys derived from those titles
- Scan for commented-out `it`, `test`, and `describe` blocks during dead-spec cleanup waves.
- End cleanup waves with repo scans for:
  - skipped tests
  - commented-out tests
  - cross-spec imports
  - placeholder titles
  - non-allowlisted `createPlateEditor` seams
- Use `bun run test:profile` for the fast suite when deciding whether a spec belongs in the slow lane. `pnpm test:slowest` and `pnpm check` enforce those thresholds.
- For rule-override hotspots, extract one editor helper and table-drive repeated node-type cases instead of cloning the same transform assertions.
- For plugin-composition hotspots, extract helpers before adding more inline setup.
- Adapt upstream invariants when local runtime semantics differ. Keep the invariant, rewrite the fixture around the real public contract.
- Treat tiny one-branch crumbs as crumbs. Do not let a coverage number talk you into fake work.
- Penalize giant files with poor test ROI unless they hold a central contract you actually care about during the next rewrite.

## Package Rules

### `autoformat`

- Use package-local rule arrays.
- Use `KEYS` or base plugins, not React plugin `.key`.
- Add only the base plugins a rule actually needs.
- Collapse tiny mark or block suites into matrices when the contract is the same.
- Do not import `AutoformatKit` or app registries in package tests.
- If an `autoformat` case needs cross-package behavior like `code-block` wiring or app-owned integration setup, move it to `apps/www/src/__tests__/package-integration` instead of expanding `packages/autoformat/package.json`.

### `markdown`

- Configure `MarkdownPlugin` locally in the package helper.
- Do not import `MarkdownKit` or any app registry from `apps/www`.
- Prefer direct string assertions for tiny whitespace-sensitive serializer outputs.

### `ai / streaming markdown`

- Keep one mixed-document smoke case.
- Add a few explicit chunk-boundary tests.
- Do not hide streaming behavior behind snapshots or giant hand-written trees.

### `core`

- Use `createSlateEditor` for:
  - pure plugin option stores
  - selector extension
  - plugin API composition
  - transform composition
  - parser and deserializer contracts
  - HTML `insertData`
  - DnD-style contracts
- Grow the compile-only type lane here first:
  - plugin creation
  - editor creation
  - inference
  - option merging
  - API merging
- Port upstream Slate React invariants by behavior, not by file.
- When a core source test mounts `Plate` while the same run also loads public-package React entrypoints, treat duplicate-instance warnings as test noise. Suppress them in the test wrapper with `suppressInstanceWarning` instead of changing runtime warning logic.
- For provider-only React specs in core, reuse the shared `packages/core/src/react/__tests__/TestPlate.tsx` helper instead of re-declaring local `const Plate = ...` wrappers.

### `selection`

- `moveSelection` and `shiftSelection` stay on Plate. They are genuinely Plate-bound exceptions, not cleanup debt.

### `docx`, `docx-io`, and app integration

- Keep app-owned cross-package integration tests under `apps/www/src/__tests__/package-integration`.
- Keep buckets local under that folder instead of scattering app-owned integration coverage through `src/lib`.
- Package tests must not pull app aliases, app kits, or registries into package graphs.
- Fixture-heavy `docx` and `docx-io` suites are valid reasons to keep `__tests__/`.

### `slate`

- Focus on pure editor, query, and transform behavior first.
- Keep runtime coverage on navigation, selection math, structural queries, transform edge cases, extension transforms, and `createEditor` legacy sync.
- Keep a small compile-only type lane for public `@platejs/slate` contracts.
- Use selective upstream mining. Pull invariants that cheaply improve local public-contract coverage; do not mirror upstream blindly.
- Add direct helper specs for custom Slate code when indirect coverage is lying.
- Use `lcov` as package truth. Bun’s text coverage summary is noisy for targeted package runs.
- Stop once the remaining misses are mostly deferred DOM wrappers plus low-risk non-DOM dust.
- Later utility and core work should mine these upstream `slate-react` invariants:
  - `use-slate-selector`: selector equality and stale-rerender prevention
  - `use-slate`: editor version and subscription behavior
  - `use-selected`: selection rerender and path stability
  - `editable`: value-change vs selection-change partitioning
  - `decorations`: decoration propagation and redecorate behavior
  - `chunking`: chunk or index invalidation only if remaining core gaps justify it
- Skip `react-editor` DOM focus coverage unless a real Plate bug forces it.
- Playwright example coverage stays out.

## Reviewed Exceptions

### `createPlateEditor` allowlist

Keep `createPlateEditor` when the contract is actually about:

- React or provider wiring
- rendered output or DOM behavior
- store rerender semantics
- Plate plugin conversion boundaries
- the known Plate-only selection APIs: `moveSelection` and `shiftSelection`

Do not treat these files as backlog just because they still use Plate.

### `__tests__/` allowlist

Keep `__tests__/` when it holds:

- package-local helpers or fixture banks
- intentionally split multi-file suites like `withAutoformat`
- fixture-heavy integration suites like `docx` and `docx-io`
- app-owned cross-package integration suites under `apps/www/src/__tests__/package-integration`

## Quick Reference

- Start with the smallest seam that proves the contract: `createEditor` -> `createSlateEditor` -> `createPlateEditor`.
- Work in passes: `>= 6` first, rerun coverage, then worthwhile `>= 5`, then switch to architecture-safety targets.
- Use `bun run test` for the fast default loop. Use `pnpm test:all` for the full suite.
- Use `bun run test:profile` to inspect the fast loop and `bun run test:slowest` to enforce it.
- `pnpm test:slowest` has two bands: warning zone for local drift and hard CI failure thresholds. Fix the warning zone before it becomes a PR failure.
- Use Bun globals. Do not import them from `bun:test`.
- Keep specs beside the implementation. Use `__tests__/` only for helpers, fixtures, or intentional split or integration suites.
- Use plain objects for simple state. Use JSX hyperscript only when tree or selection shape is the contract.
- Prefer explicit assertions. Use render-returned queries over `screen`, and use direct style-property assertions over `toHaveStyle`.
- Delete skips, commented tests, dead smoke tests, and stale snapshot files after broad title renames.
- Package tests must stay package-local. No app registries, no app kits, no cross-spec imports.
- For package-only coverage decisions, trust `lcov`, not Bun’s broad text summary.
- Keep exactly two lanes:
  - fast lane: `*.spec.ts[x]`, run by `pnpm test`
  - slow lane: `*.slow.ts[x]`, run by `pnpm test:slow`
- Use `pnpm test:all` for the full repo test run instead of relying on bare `bun test`.
- Stop before 100%. When the remaining misses are wrappers, DOM/provider dust, sludge, or crumbs, you are done.

