Skip to content

Commit 1c47843

Browse files
committed
docs: update fuz-stack testing patterns
1 parent 11b3be3 commit 1c47843

5 files changed

Lines changed: 1471 additions & 601 deletions

File tree

skills/fuz-stack/SKILL.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,15 @@ file's location on disk.
385385

386386
## Testing
387387

388-
Tests live in `src/test/` (NOT co-located). Core repos (fuz_app, fuz_ui,
389-
fuz_util) prefer `assert` from vitest — choose methods for TypeScript type
390-
narrowing, not semantic precision. Some repos (gro, zzz, fuz_css, fuz_gitops)
391-
use `expect` — follow existing convention per repo.
388+
Tests live in `src/test/` (NOT co-located). Use `assert` from vitest —
389+
choose methods for TypeScript type narrowing, not semantic precision.
390+
`assert(x instanceof Error)` narrows the type;
391+
`expect(x).toBeInstanceOf(Error)` does not. Some repos have legacy `expect`
392+
usage — prefer `assert` in new code and migrate opportunistically. Name
393+
custom assertion helpers `assert_*` (not `expect_*`).
394+
395+
Use `describe` blocks to organize tests — one or two levels deep is typical.
396+
Use `test()` (not `it()`).
392397

393398
Split large suites with dot-separated aspects: `{module}.{aspect}.test.ts`
394399
(e.g., `csp.core.test.ts`, `csp.security.test.ts`). Database tests use

skills/fuz-stack/references/testing-patterns.md

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ Testing conventions for the Fuz stack: vitest usage, fixtures, mocks, helpers.
44

55
## Contents
66

7-
- [File Organization](#file-organization) (naming, subdirectories, assertions, jsdom)
7+
- [File Organization](#file-organization) (naming, subdirectories, assertions, async rejection, jsdom)
88
- [Database Testing](#database-testing) (PGlite, vitest projects, describe_db)
99
- [Test Helpers](#test-helpers)
1010
- [Shared Test Factories](#shared-test-factories)
1111
- [Fixture-Based Testing](#fixture-based-testing)
1212
- [Mock Patterns](#mock-patterns)
1313
- [Environment Flags](#environment-flags)
14-
- [Test Structure](#test-structure) (basic, async, parameterized)
14+
- [Test Structure](#test-structure) (basic, organization, parameterized)
1515
- [Quick Reference](#quick-reference)
1616

1717
## File Organization
@@ -68,8 +68,8 @@ Real examples:
6868

6969
### Assertions
7070

71-
Prefer `assert` from vitest in core repos (fuz_app, fuz_ui, fuz_util). Choose
72-
methods for TypeScript type narrowing, not semantic precision:
71+
Use `assert` from vitest. Choose methods for TypeScript type narrowing, not
72+
semantic precision:
7373

7474
```typescript
7575
import {test, assert} from 'vitest';
@@ -79,23 +79,38 @@ assert.strictEqual(a, b);
7979
assert.deepStrictEqual(a, b);
8080
```
8181

82+
**Why `assert` over `expect`:** `assert` methods narrow types for TypeScript.
83+
`expect` chains don't:
84+
85+
```typescript
86+
// assert narrows — no type error
87+
const result: string | Error = await get_result();
88+
assert(result instanceof Error);
89+
result.message; // TypeScript knows this is Error
90+
91+
// expect doesn't narrow — type error on .message
92+
expect(result).toBeInstanceOf(Error);
93+
result.message; // Property 'message' does not exist on type 'string | Error'
94+
```
95+
8296
After `assert.isDefined(x)`, the type is `NonNullable<T>` — no `!` needed:
8397

8498
```typescript
8599
assert.isDefined(result);
86100
assert.strictEqual(result.id, expected_id); // no result! needed
87101
```
88102

89-
Some repos (gro, zzz, fuz_css, fuz_gitops) use `expect` — follow existing
90-
convention per repo. For new projects, prefer `assert`.
103+
Some repos have legacy `expect` usage — prefer `assert` in new code and
104+
migrate opportunistically.
105+
106+
Name custom assertion helpers `assert_*` (not `expect_*`).
107+
Example: `assert_result_ok()` not `expect_ok()`.
91108

92109
For throw assertions, use `assert.throws()` with Error constructor, string,
93110
or RegExp. **Do not pass a function predicate** — causes
94111
`"errorLike is not a constructor"`:
95112

96113
```typescript
97-
import {test, assert} from 'vitest';
98-
99114
// Good — RegExp matching
100115
assert.throws(() => fn(), /expected message/);
101116

@@ -120,7 +135,46 @@ try {
120135
}
121136
```
122137

123-
Same try/catch pattern works for async rejects (with `await`).
138+
### Async Rejection Testing
139+
140+
For async functions that should reject, use an `assert_rejects` helper to
141+
avoid repetitive try/catch boilerplate. Place `assert.fail` outside the catch
142+
block to prevent accidentally catching assertion errors from the test itself:
143+
144+
```typescript
145+
const assert_rejects = async (fn: () => Promise<unknown>, pattern: RegExp): Promise<Error> => {
146+
try {
147+
await fn();
148+
} catch (err) {
149+
assert(err instanceof Error); // narrows type
150+
assert.match(err.message, pattern);
151+
return err;
152+
}
153+
assert.fail('Expected to throw');
154+
};
155+
```
156+
157+
Usage:
158+
159+
```typescript
160+
// Simple — just check the error pattern
161+
await assert_rejects(
162+
() => local_repo_load({local_repo_path, git_ops, npm_ops}),
163+
/Failed to pull.*unstaged changes/,
164+
);
165+
166+
// With additional assertions on the returned error
167+
const err = await assert_rejects(
168+
() => local_repos_load({local_repo_paths: paths, git_ops, npm_ops}),
169+
/Failed to load 2 repos/,
170+
);
171+
assert.include(err.message, 'repo-a');
172+
assert.include(err.message, 'repo-b');
173+
```
174+
175+
This helper is currently defined locally in test files that need it. A future
176+
`@fuzdev/fuz_util/testing.js` module may provide this and other shared test
177+
assertions (see [Quick Reference](#quick-reference)).
124178

125179
### jsdom Environment
126180

@@ -704,6 +758,33 @@ describe('account queries', () => {
704758
});
705759
```
706760

761+
### Test Organization
762+
763+
Use `describe` blocks to organize tests. One level is common; two levels
764+
(feature → scenario) is typical for larger modules. Use `test()` not `it()`.
765+
766+
```typescript
767+
// one level — most modules
768+
describe('format_duration', () => {
769+
test('zero returns 0s', () => { ... });
770+
test('mixed units', () => { ... });
771+
});
772+
773+
// two levels — larger modules with distinct behaviors
774+
describe('local_repo_load', () => {
775+
describe('error propagation', () => {
776+
test('pull failure includes message', async () => { ... });
777+
test('checkout failure includes message', async () => { ... });
778+
});
779+
describe('skip behaviors', () => {
780+
test('local-only repos skip pull', async () => { ... });
781+
});
782+
});
783+
```
784+
785+
Flat top-level `test()` calls without `describe` are fine for very small
786+
files, but `describe` is the default.
787+
707788
### Parameterized Tests
708789

709790
Labeled tuple types for self-documenting test tables:
@@ -792,10 +873,14 @@ configuration with `session_options` and `create_route_specs`.
792873
| `fixtures/feature/case/` | Subdirectory per fixture case |
793874
| `fixtures/update.task.ts` | Parent: runs all child update tasks |
794875
| `fixtures/feature/update.task.ts` | Child: regenerates one feature |
795-
| `assert` from vitest | Preferred in core repos; follow existing convention per repo |
876+
| `assert` from vitest | Target style; some repos have legacy `expect` |
796877
| `assert.isDefined(x); x.prop` | Narrows to NonNullable — no `x!` needed |
878+
| `assert(x instanceof T); x.prop` | Narrows union types — the key advantage over `expect` |
797879
| `assert.throws(fn, /regex/)` | Returns void; second arg: constructor/string/RegExp (not function) |
798-
| try/catch + `assert.include` | For inspecting thrown errors or async rejects |
880+
| `assert_rejects(fn, /regex/)` | Async rejection helper — returns Error for further assertions |
881+
| try/catch + `assert.include` | For inspecting thrown errors when helper isn't enough |
882+
| `assert_*` (not `expect_*`) | Custom assertion helper naming convention |
883+
| `describe` + `test` (not `it`) | Default structure; 1-2 levels of `describe` typical |
799884
| `// @vitest-environment jsdom` | Pragma for UI tests needing DOM |
800885
| `vi.stubGlobal('ResizeObserver')` | Required in jsdom for components using ResizeObserver |
801886
| `describe_db(name, fn)` | DB test wrapper (fuz_app) |

0 commit comments

Comments
 (0)