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/dist/tutuca-cli.js +3 -6
- package/dist/tutuca-dev.js +49 -5
- package/dist/tutuca-dev.min.js +1 -1
- package/dist/tutuca-extra.js +3 -5
- package/dist/tutuca-extra.min.js +1 -1
- package/dist/tutuca.js +3 -5
- package/dist/tutuca.min.js +1 -1
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +5 -3
- package/skill/tutuca/core.md +9 -4
- package/skill/tutuca/testing.md +266 -0
package/package.json
CHANGED
package/skill/tutuca/SKILL.md
CHANGED
|
@@ -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
|
|
45
|
-
task touches them —
|
|
46
|
-
you'll be pointed there when
|
|
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.
|
package/skill/tutuca/core.md
CHANGED
|
@@ -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).
|
|
14
|
-
>
|
|
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.
|
|
41
|
-
|
|
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.
|