tutuca 0.9.52 → 0.9.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.52",
3
+ "version": "0.9.53",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -40,7 +40,9 @@ done:
40
40
  | Authoring `component({...})`, `html\`...\`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
41
41
  | CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
42
42
  | Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
43
+ | Authoring tests — `getTests` shape, calling methods/input/receive/bubble/response/alter handlers, designing handlers for testability | [testing.md](./testing.md) |
43
44
 
44
- Read `core.md` first. Reach for `cli.md` or `advanced.md` only when the
45
- task touches them — both files are referenced inline from `core.md` so
46
- you'll be pointed there when relevant.
45
+ Read `core.md` first. Reach for `cli.md`, `advanced.md`, or
46
+ `testing.md` only when the task touches them — all three are
47
+ referenced inline from `core.md` so you'll be pointed there when
48
+ relevant.
@@ -10,8 +10,11 @@ the `tutuca` CLI.
10
10
  > Advanced topics (drag & drop, dynamic bindings `*x`, pseudo-`x` for
11
11
  > `<select>`/`<table>`/`<tr>`, custom seq types, Tailwind/MargaUI
12
12
  > compilation): see [advanced.md](./advanced.md). CLI commands, flags,
13
- > exit codes, and full linter rule list: see [cli.md](./cli.md). Read
14
- > those only when the task touches them.
13
+ > exit codes, and full linter rule list: see [cli.md](./cli.md).
14
+ > Authoring tests `getTests` shape, calling methods/input/receive/
15
+ > bubble/response/alter handlers, designing for testability: see
16
+ > [testing.md](./testing.md). Read those only when the task touches
17
+ > them.
15
18
 
16
19
  ## Verifying changes
17
20
 
@@ -37,8 +40,10 @@ edit done:
37
40
  tutuca <module-path> test --grep "inc()" # one path
38
41
 
39
42
  Exits `4` on any failure. Skip this step when the change is purely
40
- templates/styling — `render` already covers that. Full reference,
41
- including a worked `getTests()` export, in [cli.md](./cli.md).
43
+ templates/styling — `render` already covers that. Authoring patterns
44
+ (handler calling convention, designing handlers for testability,
45
+ worked `getTests` export) in [testing.md](./testing.md); CLI flags
46
+ and exit codes in [cli.md](./cli.md).
42
47
 
43
48
  3. **Render the example(s) that exercise the feature you changed** —
44
49
  confirms the component actually mounts in a headless DOM with the new
@@ -0,0 +1,266 @@
1
+ # Tutuca Cheatsheet — Testing
2
+
3
+ How to author component tests in Tutuca: the `getTests` export shape,
4
+ the calling conventions for methods and handler blocks (`input`,
5
+ `receive`, `bubble`, `response`, `alter`), and the view-handler design
6
+ rule that keeps tests free of fake DOM events. Run them with
7
+ `tutuca <module-path> test` — flags and exit codes are in
8
+ [cli.md](./cli.md). General authoring lives in
9
+ [core.md](./core.md).
10
+
11
+ ## Setup
12
+
13
+ A module opts into `tutuca test` by exporting `getTests`:
14
+
15
+ ```js
16
+ export function getTests({ describe, test, expect }) {
17
+ describe(MyComp, () => {
18
+ test("does the thing", () => {
19
+ expect(MyComp.make().doTheThing().count).to.equal(1);
20
+ });
21
+ });
22
+ }
23
+ ```
24
+
25
+ - `expect` is chai.
26
+ - `test` and `describe` are **Tutuca's own** subset of the common
27
+ Mocha/Bun-style API, injected by `tutuca test` — not Bun's built-ins.
28
+ Available calls: `describe(title, fn)`, `describe(Component, fn)`,
29
+ `describe(title, { component }, fn)`, and `test(title, fn)`. There is
30
+ no `before` / `after` / `beforeEach` / `it` / skip-flag — don't reach
31
+ for them.
32
+ - `describe(Component, fn)` auto-tags the suite with `Component.name`,
33
+ so `tutuca <module> test Component` picks it up. Untagged `test(...)`
34
+ inside a tagged `describe` inherits the tag.
35
+
36
+ Run with `tutuca <module-path> test [name] [--grep <pattern>] [--bail]`.
37
+ Full flag/format/exit-code reference in [cli.md](./cli.md).
38
+
39
+ ## What to test
40
+
41
+ Run tests when the change is observable from JS — methods, handlers,
42
+ factories, coercion in `make({...})`. Skip them for pure
43
+ template/styling tweaks; `tutuca <module> render` covers those.
44
+
45
+ - **Methods** — call directly: `Comp.make({...}).method(args)`. Assert
46
+ on the *returned* instance (Tutuca state is immutable).
47
+ - **Input handlers** — call via
48
+ `Comp.input.handlerName.call(comp, ...args)` (see *Calling input
49
+ handlers* below).
50
+ - **All other handler kinds** (`receive`, `bubble`, `response`, `alter`,
51
+ and `on` if a component declares one) follow the **same shape**:
52
+ `Comp.<kind>.handlerName.call(comp, ...declaredArgs)`. Only the
53
+ arguments the handler receives differ:
54
+ - `receive.<name>(ctx)` — `ctx` carries `send` / `request` / `bubble`.
55
+ - `bubble.<name>(payload, ctx)` — `payload` is whatever the child sent.
56
+ - `response.<name>(res, err, ctx)` — async result + error.
57
+ - `alter.<name>(...)` — iteration handlers used by `@when`,
58
+ `@loop-with`, `@enrich-with`. Each kind has its own signature; see
59
+ *Testing iteration handlers* below.
60
+ Pass a plain stand-in for `ctx` (e.g. `{}`) when the handler doesn't
61
+ read from it; otherwise build the minimal shape it touches.
62
+ - **Factories / coercion** — `Comp.make({...})` shape, defaults, and
63
+ any deep-coercion you wired up.
64
+
65
+ ## Calling input handlers
66
+
67
+ Pattern:
68
+
69
+ ```js
70
+ Comp.input.handlerName.call(comp, arg1, arg2, /* … */);
71
+ ```
72
+
73
+ - Why `.call`: input handlers are plain functions stored on the
74
+ component descriptor. `this` must be bound explicitly to the instance.
75
+ - `comp` is an instance — `Comp.make({...})`.
76
+ - The args after `comp` are exactly what the template would have passed
77
+ (see next section). The auto-appended `ctx` is *not* required in
78
+ tests when the handler doesn't read from it; pass `{}` or a stub if
79
+ it does.
80
+ - Returned value is the next instance.
81
+
82
+ ## Testing iteration handlers
83
+
84
+ `alter` handlers run inside `@each` / `@when` / `@loop-with` /
85
+ `@enrich-with` and have three distinct shapes:
86
+
87
+ - `loopWith(seq)` — called once with the collection, returns an
88
+ `iterData` object. `this` is the parent component instance.
89
+ - `when(key, value, iterData)` — called per element, returns truthy to
90
+ keep. `this` is the parent component instance.
91
+ - `enrichWith(binds, key, value, iterData)` — called per kept element;
92
+ mutates `binds` (which already contains `key` and `value`). `this` is
93
+ the parent component instance.
94
+
95
+ You can call each one directly with `.call(comp, ...)`, but in practice
96
+ you want to test them as a pipeline: filter + loop-data + enrichment
97
+ together produce a list of bindings the view sees. Use
98
+ `collectIterBindings` from `tutuca/dev` for that:
99
+
100
+ ```js
101
+ import { collectIterBindings } from "tutuca/dev";
102
+
103
+ const c = MyComp.make({ items: [...] });
104
+ const r = collectIterBindings(MyComp, c, c.items, {
105
+ loopWith: "loopHandlerName", // optional
106
+ when: "whenHandlerName", // optional
107
+ enrichWith: "enrichHandlerName", // optional
108
+ });
109
+ // r is Array<{ key, value, ...enrichments }> — one entry per kept item,
110
+ // in iteration order.
111
+ ```
112
+
113
+ - `seq` can be a plain JS Array, a JS `Map`, or any immutable.js indexed
114
+ or keyed seq.
115
+ - Handler names refer to entries in `MyComp.alter`. An unknown name
116
+ throws — there's no silent fallback.
117
+ - The `compInstance` is `this` for every handler. Pass
118
+ `MyComp.make({ field: ... })` so handlers that read `this.field` see
119
+ the value you want.
120
+
121
+ Example:
122
+
123
+ ```js
124
+ const Items = component({
125
+ name: "Items",
126
+ fields: { items: [], multiplier: 1 },
127
+ alter: {
128
+ loopMeta(seq) { return { len: seq.length, doubled: seq.length * 2 }; },
129
+ keepEven(k) { return k % 2 === 0; },
130
+ addLabel(binds, k, v, { len }) { binds.label = `${k}/${len}: ${v}`; },
131
+ },
132
+ });
133
+
134
+ test("filters and enriches", () => {
135
+ const c = Items.make({ items: [10, 20, 30, 40] });
136
+ const r = collectIterBindings(Items, c, c.items, {
137
+ loopWith: "loopMeta",
138
+ when: "keepEven",
139
+ enrichWith: "addLabel",
140
+ });
141
+ expect(r).to.deep.equal([
142
+ { key: 0, value: 10, label: "0/4: 10" },
143
+ { key: 2, value: 30, label: "2/4: 30" },
144
+ ]);
145
+ });
146
+ ```
147
+
148
+ Use this whenever the iteration logic is the subject under test —
149
+ no DOM, no view, no Stack/Renderer needed. For end-to-end checks that
150
+ the view actually wires these handlers correctly, use
151
+ `tutuca <module> render` instead.
152
+
153
+ ## Designing handlers so tests stay simple
154
+
155
+ Tutuca templates resolve handler args by name (see
156
+ [core.md](./core.md) *Event Handling*). When you author a handler,
157
+ **pick the most specific named args you need; don't take the raw
158
+ event**. With named args, the test passes a literal; with `event`,
159
+ the test must fabricate a DOM-event-shaped object.
160
+
161
+ The dot-prefix in the template picks the handler block: a leading `.`
162
+ means "method on `this`", no dot means an input handler. The same
163
+ named-arg rule applies to both. Both forms below are correct
164
+ placements — what matters is what argument the handler asks for.
165
+
166
+ **Bad — method:**
167
+
168
+ ```html
169
+ <input @on.input=".setName event" />
170
+ ```
171
+ ```js
172
+ methods: { setName(event) { return this.setName(event.target.value); } }
173
+ ```
174
+
175
+ **Good — method:**
176
+
177
+ ```html
178
+ <input @on.input=".setName value" />
179
+ ```
180
+ ```js
181
+ methods: { setName(value) { return this.setName(value); } }
182
+ ```
183
+
184
+ **Bad — input handler:**
185
+
186
+ ```html
187
+ <input @on.input="setCount event" />
188
+ ```
189
+ ```js
190
+ input: { setCount(event) { return this.setCount(parseInt(event.target.value, 10)); } }
191
+ ```
192
+
193
+ **Good — input handler:**
194
+
195
+ ```html
196
+ <input @on.input="setCount valueAsInt" />
197
+ ```
198
+ ```js
199
+ input: { setCount(n) { return this.setCount(n); } }
200
+ ```
201
+
202
+ At test time, the "good" forms become trivial:
203
+
204
+ ```js
205
+ expect(MyComp.make().setName("Ada").name).to.equal("Ada");
206
+ expect(MyComp.input.setCount.call(MyComp.make(), 42).count).to.equal(42);
207
+ ```
208
+
209
+ The "bad" forms force every test to construct
210
+ `{ target: { value: "42" } }` (or a fuller stub when more fields are
211
+ read), which is brittle and obscures intent.
212
+
213
+ Built-in named args (full list in [core.md](./core.md) *Event
214
+ Handling*): `value`, `valueAsInt`, `valueAsFloat`, `target`, `event`,
215
+ `isAlt`, `isShift`, `isCtrl` / `isCmd`, `key`, `keyCode`, `isUpKey`,
216
+ `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`, `dragInfo`.
217
+ `ctx` is auto-appended last. Reach for `event` only when no narrower
218
+ arg fits.
219
+
220
+ ## Worked example
221
+
222
+ A `getTests` export covering a method (`inc`), an input handler with no
223
+ args (`dec`), and an input handler with a named arg (`setCount` taking
224
+ `valueAsInt`):
225
+
226
+ ```js
227
+ export function getTests({ describe, test, expect }) {
228
+ describe(Counter, () => {
229
+ describe("inc()", () => { // method
230
+ test("returns a Counter with count + 1", () => {
231
+ expect(Counter.make().inc().count).to.equal(1);
232
+ });
233
+ test("does not mutate the original instance", () => {
234
+ const c = Counter.make({ count: 7 });
235
+ c.inc();
236
+ expect(c.count).to.equal(7);
237
+ });
238
+ });
239
+
240
+ describe("dec()", () => { // input handler, no args
241
+ test("returns a Counter with count - 1", () => {
242
+ const next = Counter.input.dec.call(Counter.make());
243
+ expect(next.count).to.equal(-1);
244
+ });
245
+ });
246
+
247
+ describe("setCount()", () => { // input handler, valueAsInt
248
+ test("sets the count from a parsed int", () => {
249
+ const next = Counter.input.setCount.call(Counter.make(), 42);
250
+ expect(next.count).to.equal(42);
251
+ });
252
+ });
253
+
254
+ test("inc and dec round-trip", () => { // untagged, inherits Counter
255
+ expect(Counter.input.dec.call(Counter.make().inc()).count).to.equal(0);
256
+ });
257
+ });
258
+ }
259
+ ```
260
+
261
+ ## See also
262
+
263
+ - [core.md](./core.md) — *Verifying changes*, *Event Handling*,
264
+ *Component Skeleton*.
265
+ - [cli.md](./cli.md) — `test` flags, exit codes, output formats,
266
+ `--grep` syntax.