aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-06 21:59:40 -0500
committerCraig Jennings <c@cjennings.net>2026-05-06 21:59:40 -0500
commit241e23457bb2619541cdad170580341529607e90 (patch)
tree4c82a5c93c3ded86b0f48c2f9edb52cbfbfedf03 /languages/typescript
parent01cc47f067da15b3c87efc61c7b4eb97eb79d7d4 (diff)
downloadrulesets-241e23457bb2619541cdad170580341529607e90.tar.gz
rulesets-241e23457bb2619541cdad170580341529607e90.zip
feat(languages): add typescript bundle (Vitest-canonical)
Mirrors the python bundle's minimal shape: one language-specific file under claude/rules/. Vitest is canonical, with brief notes for Mocha+Chai and Angular Karma legacy idioms. Covers RTL query priorities, MSW for network mocking, it.each for parametrize, async patterns, and TS-specific discipline (no any in tests, prefer satisfies, etc.).
Diffstat (limited to 'languages/typescript')
-rw-r--r--languages/typescript/claude/rules/typescript-testing.md214
1 files changed, 214 insertions, 0 deletions
diff --git a/languages/typescript/claude/rules/typescript-testing.md b/languages/typescript/claude/rules/typescript-testing.md
new file mode 100644
index 0000000..bd6933f
--- /dev/null
+++ b/languages/typescript/claude/rules/typescript-testing.md
@@ -0,0 +1,214 @@
+# TypeScript Testing Rules
+
+Applies to: `**/*.{ts,tsx}`
+
+Implements the core principles from `testing.md`. All rules there apply here —
+this file covers TypeScript-specific patterns.
+
+## Framework: Vitest (canonical)
+
+Use Vitest for new TypeScript code. It's native ESM, native TS, fastest watch
+mode, and shares its config with Vite when the project already uses it.
+
+For legacy code, the same principles apply with different idioms:
+
+- **Jest** — same `describe`/`it`/`expect` API; substitute `jest.mock` for
+ `vi.mock`. Most patterns below port directly.
+- **Mocha + Chai** (Node backends) — `describe`/`it` are the same; assertions
+ use `expect(x).to.equal(y)` instead of `expect(x).toBe(y)`. Sinon for spies.
+- **Angular + Karma** (`ng test`) — different planet. Follow Angular's testing
+ guide; the Normal/Boundary/Error category discipline still applies.
+
+Don't mix frameworks in one package. Don't introduce a second framework to
+"try it out" — pick the one that fits and commit.
+
+## Test Structure
+
+Group tests in `describe` blocks that mirror the source module. Use
+`beforeEach` for setup that every test in the block needs:
+
+```ts
+import { beforeEach, describe, expect, it } from "vitest";
+import { Cart } from "./cart";
+
+describe("Cart", () => {
+ let cart: Cart;
+
+ beforeEach(() => {
+ cart = new Cart({ userId: 42 });
+ });
+
+ it("normal: adding an in-stock item increases quantity", () => {
+ cart.add("SKU-1", 2);
+ expect(cart.itemCount("SKU-1")).toBe(2);
+ });
+
+ it("boundary: quantity 0 is a no-op, not an error", () => {
+ cart.add("SKU-1", 0);
+ expect(cart.itemCount("SKU-1")).toBe(0);
+ });
+
+ it("error: negative quantity throws", () => {
+ expect(() => cart.add("SKU-1", -1)).toThrow(/quantity must be non-negative/);
+ });
+});
+```
+
+Co-locate test files with the source under test (`cart.ts` ↔ `cart.test.ts`)
+unless the project layout dictates a separate `tests/` tree. Match the project's
+existing convention.
+
+## Fixtures via Factory Functions
+
+TypeScript has no pytest-style fixture system. Use plain factory functions for
+test data — they're typed, refactor-safe, and explicit:
+
+```ts
+const makeUser = (overrides: Partial<User> = {}): User => ({
+ id: "u-1",
+ email: "alice@example.com",
+ role: "member",
+ ...overrides,
+});
+
+it("admin can delete posts", () => {
+ const admin = makeUser({ role: "admin" });
+ expect(canDeletePosts(admin)).toBe(true);
+});
+```
+
+Avoid `faker`/`@faker-js/faker` unless the project genuinely needs random data.
+Random fixtures hide off-by-one bugs and make test failures non-reproducible.
+Seed deterministically when faker is unavoidable.
+
+## Parametrize for Category Coverage
+
+Use `it.each` to cover normal, boundary, and error cases concisely instead of
+hand-writing near-duplicate tests:
+
+```ts
+it.each<[number, boolean]>([
+ [1, true], // Normal
+ [100, true], // Normal: bulk
+ [0, true], // Boundary: zero is a no-op
+ [-1, false], // Error: negative
+])("add(SKU-1, %i) — valid=%s", (quantity, valid) => {
+ if (valid) {
+ cart.add("SKU-1", quantity);
+ } else {
+ expect(() => cart.add("SKU-1", quantity)).toThrow();
+ }
+});
+```
+
+### Pairwise / Combinatorial for Parameter-Heavy Functions
+
+When `it.each` would require listing dozens of combinations (feature flags ×
+permissions × shipping × payment × etc.), switch to combinatorial coverage via
+`/pairwise-tests`. The skill generates a minimal matrix covering every 2-way
+parameter interaction — typically 80-99% fewer cases than exhaustive,
+catching most combinatorial bugs.
+
+Workflow: invoke `/pairwise-tests` → get a PICT model + generated test matrix
+→ paste the matrix into an `it.each` block. See `testing.md` § Combinatorial
+Coverage for the general rule and when to skip.
+
+## Mocking Guidelines
+
+### Mock these (external boundaries):
+- HTTP clients (`fetch`, `axios`, `ky`) — prefer MSW (see below) over
+ client-level mocks where possible.
+- File / filesystem APIs (`fs/promises`)
+- Time (`vi.useFakeTimers()` + `vi.setSystemTime(...)`)
+- Browser globals not covered by jsdom (`navigator.geolocation`,
+ `IntersectionObserver`, `ResizeObserver`)
+- Third-party SDKs (Stripe client, AWS SDK clients)
+
+### Never mock these (internal domain):
+- Your own service, hook, or utility functions
+- Type-narrowing helpers (`isFoo`, `assertBar`) — those are the work
+- Validation libraries (`zod`, `valibot`) — they're framework, not boundary
+- React's render lifecycle, hooks, or context — use RTL to exercise the real thing
+
+If a unit test needs heavy internal mocking, the production code needs
+restructuring (see `testing.md` § *If Tests Are Hard to Write, Refactor the
+Code*).
+
+## Async Testing
+
+Use native `async`/`await`. Don't wrap in promise chains, don't mix `done`
+callbacks:
+
+```ts
+it("processOrder resolves with status 'processed'", async () => {
+ const result = await processOrder(sampleOrder);
+ expect(result.status).toBe("processed");
+});
+```
+
+For React component tests, use RTL's `findBy*` (auto-waits) over
+`waitFor(() => getBy*)` — `findBy*` is purpose-built for the "appears
+eventually" case.
+
+## React Testing Library
+
+When testing React components:
+
+- **Query priority**: `getByRole` > `getByLabelText` > `getByPlaceholderText`
+ > `getByText`. Reach for `getByTestId` only when the others genuinely don't
+ fit. Test like the user — don't query CSS classes or DOM structure.
+- **`userEvent` over `fireEvent`**. `userEvent` simulates real interaction
+ (focus, keyboard, async). `fireEvent` is a low-level escape hatch.
+- **One assertion per behavioral concern**. A test that asserts "button
+ renders" + "button submits the form" + "form clears on submit" is three
+ tests.
+- **Don't snapshot DOM**. Snapshots rot fast and reviewers rubber-stamp them.
+ Assert specific attributes and text instead.
+
+## Network Mocking: MSW
+
+For request-level mocking in component and integration tests, use MSW (Mock
+Service Worker). It intercepts at the network layer, so the app code is
+exercised end-to-end up to the boundary:
+
+```ts
+import { http, HttpResponse } from "msw";
+import { setupServer } from "msw/node";
+
+const server = setupServer(
+ http.get("/api/orders/:id", ({ params }) =>
+ HttpResponse.json({ id: params.id, status: "shipped" })
+ )
+);
+
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
+```
+
+Prefer MSW over mocking `fetch` or `axios` directly — request mocks survive
+client-library swaps; `vi.mock("axios")` doesn't.
+
+## TypeScript-Specific Discipline
+
+- **No `any` in tests.** Tests are documentation; `any` lies about the shape.
+ Use `unknown` and narrow, or define the precise type.
+- **Prefer `satisfies` over type assertions.** `as` says "trust me"; `satisfies`
+ says "verify this conforms" without widening the inferred type.
+- **Don't disable strict checks for tests.** Same `tsconfig` as production.
+ If a test needs `// @ts-expect-error`, leave a comment explaining the
+ invariant being verified.
+- **Assertion functions for invariants.** When narrowing via runtime check is
+ expressive, write `assertIsFoo(value)` (returns `asserts value is Foo`)
+ instead of casting in every test.
+
+## Anti-Patterns (TypeScript-Specific)
+
+- Casting test data with `as Whatever` to silence a type error — fix the
+ factory or the type.
+- Using `jest.mock` patterns in a Vitest project (or vice versa) — pick one.
+- Snapshot-testing JSX trees — brittle, low-signal.
+- Testing `useState` / `useReducer` directly via `renderHook` when the same
+ behavior is reachable through the component's UI — render the component.
+- `expect(x).toBeTruthy()` when you mean `expect(x).toBe(true)` — they are
+ different invariants and the looser one masks bugs.