@@ -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
7575import {test , assert } from ' vitest' ;
@@ -79,23 +79,38 @@ assert.strictEqual(a, b);
7979assert .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+
8296After ` assert.isDefined(x) ` , the type is ` NonNullable<T> ` — no ` ! ` needed:
8397
8498``` typescript
8599assert .isDefined (result );
86100assert .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
92109For throw assertions, use ` assert.throws() ` with Error constructor, string,
93110or 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
100115assert .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
709790Labeled 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