tutuca 0.11.0 → 0.11.2

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.
Files changed (116) hide show
  1. package/dist/tutuca-cli.js +264 -361
  2. package/dist/tutuca-components.js +175 -175
  3. package/dist/tutuca-dev.ext.js +250 -323
  4. package/dist/tutuca-dev.js +250 -323
  5. package/dist/tutuca-dev.min.js +4 -4
  6. package/dist/tutuca-extra.ext.js +55 -46
  7. package/dist/tutuca-extra.js +55 -46
  8. package/dist/tutuca-extra.min.js +3 -3
  9. package/dist/tutuca-storybook.js +29 -29
  10. package/dist/tutuca.ext.js +55 -46
  11. package/dist/tutuca.js +55 -46
  12. package/dist/tutuca.min.js +3 -3
  13. package/package.json +4 -3
  14. package/skill/margaui/SKILL.md +0 -105
  15. package/skill/margaui/components/accordion.md +0 -127
  16. package/skill/margaui/components/alert.md +0 -174
  17. package/skill/margaui/components/aura.md +0 -97
  18. package/skill/margaui/components/avatar.md +0 -220
  19. package/skill/margaui/components/badge.md +0 -193
  20. package/skill/margaui/components/breadcrumbs.md +0 -103
  21. package/skill/margaui/components/button.md +0 -322
  22. package/skill/margaui/components/calendar.md +0 -67
  23. package/skill/margaui/components/card.md +0 -373
  24. package/skill/margaui/components/carousel.md +0 -387
  25. package/skill/margaui/components/chat.md +0 -171
  26. package/skill/margaui/components/checkbox.md +0 -101
  27. package/skill/margaui/components/collapse.md +0 -172
  28. package/skill/margaui/components/countdown.md +0 -165
  29. package/skill/margaui/components/diff.md +0 -53
  30. package/skill/margaui/components/divider.md +0 -107
  31. package/skill/margaui/components/dock.md +0 -173
  32. package/skill/margaui/components/drawer.md +0 -184
  33. package/skill/margaui/components/dropdown.md +0 -388
  34. package/skill/margaui/components/fab.md +0 -346
  35. package/skill/margaui/components/fieldset.md +0 -88
  36. package/skill/margaui/components/file-input.md +0 -84
  37. package/skill/margaui/components/filter.md +0 -52
  38. package/skill/margaui/components/footer.md +0 -583
  39. package/skill/margaui/components/hero.md +0 -135
  40. package/skill/margaui/components/hover-3d.md +0 -129
  41. package/skill/margaui/components/hover-gallery.md +0 -49
  42. package/skill/margaui/components/indicator.md +0 -265
  43. package/skill/margaui/components/input.md +0 -389
  44. package/skill/margaui/components/join.md +0 -100
  45. package/skill/margaui/components/kbd.md +0 -127
  46. package/skill/margaui/components/label.md +0 -102
  47. package/skill/margaui/components/link.md +0 -96
  48. package/skill/margaui/components/list.md +0 -182
  49. package/skill/margaui/components/loading.md +0 -105
  50. package/skill/margaui/components/mask.md +0 -168
  51. package/skill/margaui/components/megamenu.md +0 -131
  52. package/skill/margaui/components/menu.md +0 -887
  53. package/skill/margaui/components/mockup-browser.md +0 -39
  54. package/skill/margaui/components/mockup-code.md +0 -81
  55. package/skill/margaui/components/mockup-phone.md +0 -39
  56. package/skill/margaui/components/mockup-window.md +0 -33
  57. package/skill/margaui/components/modal.md +0 -196
  58. package/skill/margaui/components/navbar.md +0 -282
  59. package/skill/margaui/components/otp.md +0 -171
  60. package/skill/margaui/components/pagination.md +0 -122
  61. package/skill/margaui/components/progress.md +0 -135
  62. package/skill/margaui/components/radial-progress.md +0 -67
  63. package/skill/margaui/components/radio.md +0 -133
  64. package/skill/margaui/components/range.md +0 -134
  65. package/skill/margaui/components/rating.md +0 -170
  66. package/skill/margaui/components/select.md +0 -225
  67. package/skill/margaui/components/skeleton.md +0 -64
  68. package/skill/margaui/components/stack.md +0 -142
  69. package/skill/margaui/components/stat.md +0 -254
  70. package/skill/margaui/components/status.md +0 -73
  71. package/skill/margaui/components/steps.md +0 -138
  72. package/skill/margaui/components/swap.md +0 -152
  73. package/skill/margaui/components/tab.md +0 -248
  74. package/skill/margaui/components/table.md +0 -1018
  75. package/skill/margaui/components/text-rotate.md +0 -91
  76. package/skill/margaui/components/textarea.md +0 -85
  77. package/skill/margaui/components/theme-controller.md +0 -266
  78. package/skill/margaui/components/timeline.md +0 -1356
  79. package/skill/margaui/components/toast.md +0 -165
  80. package/skill/margaui/components/toggle.md +0 -135
  81. package/skill/margaui/components/tooltip.md +0 -181
  82. package/skill/margaui/components/validator.md +0 -163
  83. package/skill/tutuca/SKILL.md +0 -56
  84. package/skill/tutuca/advanced.md +0 -212
  85. package/skill/tutuca/cli.md +0 -239
  86. package/skill/tutuca/component-design.md +0 -168
  87. package/skill/tutuca/core.md +0 -908
  88. package/skill/tutuca/iteration.md +0 -207
  89. package/skill/tutuca/macros.md +0 -86
  90. package/skill/tutuca/margaui.md +0 -175
  91. package/skill/tutuca/messages-and-intents.md +0 -399
  92. package/skill/tutuca/patterns/README.md +0 -48
  93. package/skill/tutuca/patterns/add-a-story.md +0 -26
  94. package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
  95. package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
  96. package/skill/tutuca/patterns/coordinate-components.md +0 -54
  97. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -27
  98. package/skill/tutuca/patterns/enrich-each-item.md +0 -25
  99. package/skill/tutuca/patterns/file-input.md +0 -39
  100. package/skill/tutuca/patterns/filter-a-list.md +0 -25
  101. package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
  102. package/skill/tutuca/patterns/handle-events.md +0 -38
  103. package/skill/tutuca/patterns/iterate-a-list.md +0 -18
  104. package/skill/tutuca/patterns/paginate-a-list.md +0 -29
  105. package/skill/tutuca/patterns/render-a-child-component.md +0 -21
  106. package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
  107. package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -38
  108. package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
  109. package/skill/tutuca/patterns/switch-between-views.md +0 -30
  110. package/skill/tutuca/patterns/tabbed-interface.md +0 -43
  111. package/skill/tutuca/semantics.md +0 -195
  112. package/skill/tutuca/storybook.md +0 -270
  113. package/skill/tutuca/styles.md +0 -48
  114. package/skill/tutuca/testing.md +0 -346
  115. package/skill/tutuca-source/SKILL.md +0 -33
  116. package/skill/tutuca-source/tutuca.ext.js +0 -4235
@@ -1,270 +0,0 @@
1
- # Tutuca — Storybook
2
-
3
- Reach this file when authoring `*.dev.js` story modules or running
4
- `tutuca storybook` — defining `getExamples()` sections, mocking intent handlers per
5
- example, or rendering a live component catalog. For the framework primer see
6
- [core.md](./core.md); for the full CLI flag/exit-code table see
7
- [cli.md](./cli.md); for the `getTests` shape see [testing.md](./testing.md).
8
-
9
- ## Mental model
10
-
11
- `tutuca storybook [dir]` recursively discovers co-located `*.dev.js` modules,
12
- mounts them via the shipped `tutuca/storybook` library, and serves an ephemeral
13
- page — no config, no HTML to write. It is **batteries-included by default**:
14
- before serving it runs each module's `getTests()` in the terminal, the page
15
- wires margaui styling, and the browser runs `check(app)` — the dev-build lint
16
- runner, which lints every registered component and logs findings to the
17
- console (a no-op stub in the core build). Each is individually
18
- disablable with a `--no-*` flag. All tutuca specifiers resolve to **one
19
- runtime** — component scope and identity require it.
20
-
21
- To embed the same catalog in your own page, use the shipped
22
- `tutuca/storybook` library: `buildStorybook(modules)` assembles the
23
- sections from conventional modules, and
24
- `mountStorybook(selector, modules, opts)` builds, mounts, and starts the
25
- app in one call.
26
-
27
- ## The `.dev.js` module
28
-
29
- A `*.dev.js` file is a **dev-only module**: it holds stories, tests, and
30
- development-time helpers for nearby components, and is **never shipped to
31
- production or the UI**. The `.dev.js` suffix is the contract — your app imports
32
- its real components directly and never a `.dev.js`, and a production build glob
33
- can exclude `**/*.dev.js`. Because it follows the full module convention, the
34
- same file is a valid target for `tutuca lint` / `test` / `render` too. (The
35
- export shape itself is
36
- [Conventional Module Exports](./core.md#conventional-module-exports) —
37
- this table adds what the storybook does with each.)
38
-
39
- | Export | Returns | Used for |
40
- | ------ | ------- | -------- |
41
- | `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included. Components dedup by identity, so re-listing a leaf that another module also lists is safe (a composition module can re-list every leaf it uses). |
42
- | `getExamples()` | one section, or an array of sections | the catalog cards |
43
- | `getTests({ describe, test, expect })` | tests | the pre-serve test run (optional) |
44
- | `getMacros()` | `{ name: macro }` | macros referenced in views (optional) |
45
- | `getIntentHandlers()` | `{ name: async fn }` | the module's **real** handlers for the `lex` leg (optional) |
46
- | `getRoot()` | `Root.make({...})` | root state when examples need it (optional) |
47
-
48
- ## Authoring stories (`getExamples`)
49
-
50
- Return one section, or an array of sections to group examples under multiple
51
- headings. A section is `{ title, description?, group?, items: [...] }`. An array
52
- of one section behaves exactly like returning that section directly — both go
53
- through the same `Section.fromData`, which **throws** on a malformed section
54
- (missing `title`, not an object) rather than rendering a placeholder title.
55
-
56
- The optional `group` is a string that clusters sidebar sections under one
57
- collapsible header: sections sharing a `group` name nest beneath it (across
58
- modules too), while groups and ungrouped sections interleave alphabetically by
59
- display key. Omit it (or pass `""`) for a flat top-level section — the default
60
- and backward-compatible behavior. A non-string `group` is a shape error.
61
-
62
- ```js
63
- import { component, html } from "tutuca";
64
- import { Counter } from "./counter.js";
65
-
66
- export function getComponents() {
67
- return [Counter];
68
- }
69
- export function getExamples() {
70
- return { // one section, or an array of these
71
- title: "Counter",
72
- description: "A button that counts clicks.", // optional
73
- items: [
74
- { title: "Basic", description: "starts at zero", value: Counter.make({ count: 0 }) },
75
- { title: "Pre-filled", value: Counter.make({ count: 5 }) },
76
- ],
77
- };
78
- }
79
- ```
80
-
81
- To cluster sections in the sidebar, return an array and give related sections
82
- the same `group`:
83
-
84
- ```js
85
- export function getExamples() {
86
- return [
87
- { group: "Inputs", title: "Counter", items: [{ title: "Basic", value: Counter.make({ count: 0 }) }] },
88
- { group: "Inputs", title: "Slider", items: [{ title: "Basic", value: Slider.make({ value: 50 }) }] },
89
- { group: "Layout", title: "Grid", items: [{ title: "Basic", value: Grid.make({}) }] },
90
- { title: "Misc", items: [{ title: "Basic", value: Misc.make({}) }] }, // ungrouped → flat
91
- ];
92
- }
93
- ```
94
-
95
- `Counter` and `Slider` nest under a collapsible **Inputs** header, `Grid` under
96
- **Layout**, and the ungrouped `Misc` sits flat at the top level; headers and
97
- loose sections interleave alphabetically.
98
-
99
- Item fields:
100
-
101
- - `title` — required.
102
- - `description?` — shown under the card title.
103
- - `value` — required, the instance to render, usually `Comp.make({...})`.
104
- - `view?` — selects a pushed named view, rendered via `@push-view` in the card.
105
- - `intentHandlers?` — per-example intent-handler mocks (next section).
106
- - `on?` — lifecycle hooks; messages sent to `value` as sections are navigated
107
- ([Lifecycle hooks](#lifecycle-hooks-on)).
108
-
109
- The storybook sorts sections by title and renders a sidebar with a filter, so
110
- one example item per meaningful state reads as a state matrix.
111
-
112
- ## Mocking intent handlers per example
113
-
114
- An item's optional `intentHandlers` map holds async functions keyed by intent
115
- name that override the module's real `getIntentHandlers()` handler **for that
116
- one example instance only** — so two examples of the same component show
117
- different answers side by side. The three idioms:
118
-
119
- ```js
120
- items: [
121
- { title: "Loaded", value: Widget.make({ isLoading: true }),
122
- intentHandlers: { async load() { return [{ id: 1, name: "Ada" }]; } } }, // fixture
123
- { title: "Error", value: Widget.make({ isLoading: true }),
124
- intentHandlers: { async load() { throw new Error("boom"); } } }, // error path
125
- { title: "Loading", value: Widget.make({ isLoading: true }),
126
- intentHandlers: { load() { return new Promise(() => {}); } } }, // never resolves
127
- { title: "Default", value: Widget.make() }, // no mock → the real handler, or nothing
128
- ]
129
- ```
130
-
131
- How it resolves: the storybook registers one meta-handler per intent name. On
132
- dispatch it walks the issuing component's path leaf→root to find the nearest
133
- example carrying a mock for that name (**nearest example wins**), else falls back
134
- to the module's real handler. With neither, the meta-handler **declines** (`PASS`)
135
- rather than inventing an error, so the walk runs out and the example hears
136
- `<name>Unhandled` — "nothing claimed it", not "a handler refused it".
137
- This is **storybook-only** — at runtime your real `getIntentHandlers()` apply.
138
- See [messages-and-intents.md](./messages-and-intents.md) for the handler contract (the
139
- `ctx` is the handler's final argument). `tutuca storybook --dry-run --json`
140
- lists each example's mocked names.
141
-
142
- ## Lifecycle hooks (`on`)
143
-
144
- An item's optional `on` field declares messages dispatched to the example's
145
- component (`value`) as the user navigates sections — for examples that need to be
146
- "kicked" into a state (load data, open a panel, focus an input) rather than
147
- constructed in it. Three phases:
148
-
149
- - **`init`** — the first time a section is displayed, sent to each of its examples.
150
- - **`resume`** — each subsequent time that section is re-displayed.
151
- - **`suspend`** — when a section is navigated away from.
152
-
153
- ```js
154
- items: [
155
- { title: "Loaded", value: Grid.make({}),
156
- on: {
157
- init: { intent: [{ name: "load", args: [], opts: { route: ["lex"] } }] }, // fetch on first show
158
- resume: { send: [{ name: "refresh", args: [] }] }, // re-poll on return
159
- suspend: { send: [{ name: "pause", args: [] }] }, // stop work when hidden
160
- } },
161
- ]
162
- ```
163
-
164
- Each phase holds **action buckets** — `send` (→ a `receive` handler) and
165
- `intent` (→ a walk along `opts.route`, answering into a `receive` arm named
166
- `<name>Ok` / `<name>Error` / `<name>Unhandled`) — each an array of
167
- `{ name, args?, opts? }`. `args` is a plain array, or a **function** `(self) =>
168
- [...]` called with the example's component instance:
169
-
170
- ```js
171
- on: { init: { send: [{ name: "select", args: (self) => [self.firstId()] }] } }
172
- ```
173
-
174
- For ordering **across** kinds, use `do` — an explicit sequence where each item
175
- carries its own `type`:
176
-
177
- ```js
178
- on: { init: { do: [
179
- { type: "send", name: "reset", args: [] },
180
- { type: "intent", name: "load", args: [], opts: { route: ["lex"] } }, // runs after reset
181
- ] } }
182
- ```
183
-
184
- `intent` actions on the `lex` leg honor the example's `intentHandlers` mocks. A
185
- phase message with no matching handler on the component is a silent no-op, and an
186
- `intent` on the `dyn` leg walks up into the storybook engine rather than into your
187
- own tree — the visibly-useful shapes are `send` and `intent` on `lex`.
188
-
189
- ## Stories as tests (`getTests`)
190
-
191
- `getTests` runs through the same machinery as `tutuca test`; the storybook runs
192
- it in the terminal before serving (skip with `--no-tests`). `describe(Comp, fn)`
193
- auto-tags the suite by `Comp.name`:
194
-
195
- ```js
196
- export function getTests({ describe, test, expect }) {
197
- describe(Counter, () => {
198
- test("starts at zero", () => expect(Counter.make({}).count).toBe(0));
199
- });
200
- }
201
- ```
202
-
203
- See [testing.md](./testing.md) for the full `getTests` shape and how to call
204
- methods / receive / intent / alter handlers.
205
-
206
- ## Running it
207
-
208
- ```sh
209
- tutuca storybook # scan + serve the current directory
210
- tutuca storybook ./packages/ui # scan + serve another directory
211
- tutuca storybook --dry-run # prep + print what would be shown, don't serve (smoke test)
212
- tutuca storybook --dry-run --json # same, machine-readable for agents
213
- tutuca storybook --out ./_site # write a static index.html + bootstrap instead of serving
214
- tutuca storybook --no-tests # skip the pre-serve getTests() run
215
- ```
216
-
217
- Runtime resolution (convention over configuration): a local
218
- `node_modules/tutuca` install if present, else the CLI's own `dist`, else the
219
- version-pinned CDN. `--out` always pins the CDN so the artifact is portable —
220
- host it from the project root so `/*.dev.js` paths resolve. See [cli.md](./cli.md)
221
- for the exhaustive flag list and exit codes.
222
-
223
- ## Themes
224
-
225
- The page starts in the theme the URL asks for (`?theme=dracula`), else the one
226
- the OS prefers, and the sidebar has a switcher for margaui's palettes. Picking
227
- one sets `?theme=`, so a themed storybook is shareable and survives a reload.
228
- That OS step is on us, not margaui: its `dark.css` is keyed on
229
- `[data-theme="dark"]` alone, with **no** `prefers-color-scheme` fallback, so a
230
- page that only links `theme.css` renders light forever. See
231
- [margaui.md](./margaui.md).
232
-
233
- Embedding the storybook yourself? The switcher is opt-in through the `themes`
234
- option, which — like `compileCss` — is injected, so the library still never
235
- imports margaui:
236
-
237
- ```js
238
- mountStorybook("#app", modules, {
239
- compileCss: (app) => compileClassesToStyleText(app, compile),
240
- // The directory your theme.css lives in. Every other palette is a sibling
241
- // file there; only the one selected is fetched, and only the first time.
242
- themes: { baseUrl: "https://marianoguerra.github.io/margaui/themes/" },
243
- });
244
- ```
245
-
246
- Omit `themes` (what `--no-margaui` does) and no switcher renders — there would
247
- be no theme CSS to switch to.
248
-
249
- ## Footguns
250
-
251
- - ⚠️ `value` must be a real instance (`Comp.make(...)`), not a plain object or
252
- the class itself — examples need an addressable instance for event dispatch.
253
- - ⚠️ `intentHandlers` mocks are **storybook-only** and per-instance; don't rely
254
- on them in `getTests` or production code.
255
- - ⚠️ Never import a `.dev.js` from app/production code — the suffix is the
256
- ship / no-ship boundary.
257
- - ⚠️ An example whose component raises an intent with no real handler and no
258
- per-example mock hears `<name>Unhandled` — and if it declares no answer arm at
259
- all, nothing happens and nothing is reported. Declare `<name>Unhandled` (or at
260
- least `<name>Error`) while wiring one up.
261
- - ⚠️ Keep one tutuca runtime — mixed specifiers or installs break scope identity.
262
-
263
- ## Verify
264
-
265
- The standard recipe
266
- ([Verifying changes](./core.md#verifying-changes)) plus a storybook
267
- dry run: `tutuca lint <module>.dev.js` → `tutuca test <module>.dev.js` →
268
- `tutuca storybook --dry-run --json <dir>` (smoke-test discovery, counts,
269
- and mocked names without serving), then `tutuca storybook <dir>` to view
270
- it live.
@@ -1,48 +0,0 @@
1
- # Tutuca — Styles
2
-
3
- Read this file when authoring `style` / `commonStyle` / `globalStyle`
4
- blocks or debugging CSS that silently doesn't apply.
5
-
6
- ```js
7
- component({
8
- style: css`.mine { color: red; }`, // scoped to main view
9
- commonStyle: css`.shared { color: yellow; }`, // scoped to all views of this component
10
- globalStyle: css`.app-thing { color: green; }`, // global, no scoping
11
- views: {
12
- two: { view: html`...`, style: css`.mine { color: orange; }` },
13
- },
14
- });
15
- ```
16
-
17
- Tagged templates `html` and `css` are just `String.raw` (editor hinting
18
- only). Plain strings work too.
19
-
20
- `style` and `commonStyle` are wrapped in a component-scoped selector
21
- (`[data-cid="N"]{ … }`), so their CSS lands *inside* a style-rule block.
22
-
23
- A useful consequence: **bare declarations with no selector** (e.g.
24
- `color: red; padding: 1rem;`) land directly inside that wrapper, so they style
25
- the component's **root element** — the host node carrying `data-cid` (plus
26
- `data-vid` for a per-view `style`). Reach for this to style a component's own
27
- outer element without adding a wrapper selector; nested rules with a selector
28
- (`.mine { … }`) target descendants instead.
29
-
30
- Because the CSS sits inside a style-rule block,
31
- top-level-only constructs break there and the browser silently drops them —
32
- put them in `globalStyle` (injected verbatim, no wrapper) instead:
33
-
34
- - Non-nestable at-rules: `@import`, `@charset`, `@namespace`, `@font-face`,
35
- `@keyframes`, `@page`, `@property`, `@counter-style`, `@font-feature-values`,
36
- `@font-palette-values`, `@view-transition`. (Conditional group rules —
37
- `@media`, `@supports`, `@container`, `@layer`, `@scope`, `@starting-style` —
38
- *do* nest and stay in `style`/`commonStyle`.)
39
- - Rules whose leading selector is `html`, `body`, or `:root`: once scoped they
40
- become descendant selectors that never match.
41
-
42
- The linter flags both (`TOP_LEVEL_AT_RULE_IN_SCOPED_STYLE`,
43
- `GLOBAL_SELECTOR_IN_SCOPED_STYLE`). For a genuine false positive, put a
44
- `/* tutuca-lint-ignore */` comment on the same line as the flagged construct.
45
-
46
- For Tailwind / MargaUI utility classes (compiling `class=` literals into
47
- CSS via the extra build) and the `compileClassesToStyleText` + `injectCss`
48
- wiring, see [margaui.md](./margaui.md).
@@ -1,346 +0,0 @@
1
- # Tutuca — Testing
2
-
3
- How to author component tests in Tutuca: the `getTests` export shape,
4
- the calling conventions for methods and handler blocks (`receive`,
5
- `intent`, `alter`), and the view-handler design
6
- rule that keeps tests free of fake DOM events. Run them with
7
- `tutuca test <module-path>` — 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).toBe(1);
20
- });
21
- });
22
- }
23
- ```
24
-
25
- - `expect` is chai, extended with **jest-style matchers** (`toBe`,
26
- `toEqual`, `toContain`, `toThrow`, `.not.toBe`, …) — the recommended
27
- style. Chai's BDD chain (`expect(x).to.equal(1)`) still works for those
28
- who prefer it. Run `tutuca help` for the full matcher list (it's
29
- surfaced from code). Asymmetric/mock matchers
30
- (`expect.objectContaining`, `toHaveBeenCalled…`, `toMatchSnapshot`) are
31
- **not** available — tutuca has no mocking layer.
32
- - `test` and `describe` are **Tutuca's own** subset of the common
33
- Mocha/Jest-style API, injected by `tutuca test` — they are not imported
34
- from a test runner, so don't reach for one's extras.
35
- Available calls: `describe(title, fn)`, `describe(Component, fn)`,
36
- `describe(title, { component }, fn)`, and `test(title, fn)`. There is
37
- no `before` / `after` / `beforeEach` / `it` / skip-flag — don't reach
38
- for them.
39
- - `describe(Component, fn)` auto-tags the suite with `Component.name`,
40
- so `tutuca test <module> Component` picks it up. Untagged `test(...)`
41
- inside a tagged `describe` inherits the tag.
42
-
43
- Run with `tutuca test <module-path> [name] [--grep <pattern>] [--bail]`.
44
- Full flag/format/exit-code reference in [cli.md](./cli.md).
45
-
46
- ## What to test
47
-
48
- Run tests when the change is observable from JS — methods, handlers,
49
- factories, coercion in `make({...})`. Skip them for pure
50
- template/styling tweaks; `tutuca render <module>` covers those.
51
-
52
- - **Methods** — call directly: `Comp.make({...}).method(args)`. Assert
53
- on the *returned* instance (Tutuca state is immutable).
54
- - **Receive handlers** — call via
55
- `Comp.receive.handlerName.call(comp, ...args)` (see *Calling receive
56
- handlers* below). One bucket holds every addressed message, so the same
57
- call drives a view's `@on.*` name, what a parent sends, and an
58
- **answer** to an intent — a handler cannot tell them apart, which is
59
- exactly what makes it testable:
60
- - `receive.<name>(ctx)` — `ctx` carries `send` / `intent` / `forward`.
61
- - `receive.<name>Ok(res, ctx)` / `receive.<name>Error(err, ctx)` /
62
- `receive.<name>Unhandled(...intentArgs, ctx)` — the three outcomes
63
- of an intent. Each takes **one** payload, so there is no arm that
64
- can be handed both a result and an error.
65
- - **The other handler kinds** (`intent`, `alter`) follow the **same
66
- shape**: `Comp.<kind>.handlerName.call(comp, ...declaredArgs)`. Only
67
- the arguments differ:
68
- - `intent.<name>(payload, ctx)` — `payload` is whatever the sender
69
- raised it with. Call `ctx.reply` / `ctx.fail` on a stand-in ctx to
70
- assert what it answers.
71
- - `alter.<name>(...)` — iteration handlers used by `@when`,
72
- `@loop-with`, `@enrich-with`. Each kind has its own signature; see
73
- *Testing iteration handlers* below.
74
- Pass a plain stand-in for `ctx` (e.g. `{}`) when the handler doesn't
75
- read from it; otherwise build the minimal shape it touches.
76
- - **Factories / coercion** — `Comp.make({...})` shape, defaults, and
77
- any deep-coercion you wired up.
78
-
79
- ## Calling receive handlers
80
-
81
- Pattern:
82
-
83
- ```js
84
- Comp.receive.handlerName.call(comp, arg1, arg2, /* … */);
85
- ```
86
-
87
- - Why `.call`: receive handlers are plain functions stored on the
88
- component descriptor. `this` must be bound explicitly to the instance.
89
- - `comp` is an instance — `Comp.make({...})`.
90
- - The args after `comp` are exactly what the template would have passed
91
- (see next section). The auto-appended `ctx` is *not* required in
92
- tests when the handler doesn't read from it; pass `{}` or a stub if
93
- it does.
94
- - Returned value is the next instance.
95
-
96
- ## Driving a full cascade (`drive`)
97
-
98
- Direct `.call(comp, ...)` tests one handler in isolation. When you need a message
99
- to fan out through real dispatch — a `request` that resolves and feeds its
100
- an intent's answer, a `send` that triggers more sends — `getTests` also injects an async
101
- `drive` helper (alongside `describe`, `test`, `expect`):
102
-
103
- ```js
104
- export function getTests({ describe, test, expect, drive }) {
105
- describe(Grid, () => {
106
- test("init loads rows", async () => {
107
- const settled = await drive(
108
- Grid.make({ rows: [] }),
109
- { request: [{ name: "load", args: [] }] }, // an `on`-phase config
110
- );
111
- expect(settled.rows.size).toBe(3);
112
- });
113
- });
114
- }
115
- ```
116
-
117
- - `drive(value, phase, opts?)` builds a transactor over `value`, dispatches the
118
- phase's actions at the root, awaits the whole cascade (including async
119
- requests), and returns the **settled** instance.
120
- - `drive` **always originates at the root** — there is no `at:`/path option. To
121
- exercise a handler on a nested child, call it directly with `.call(child, …)`.
122
- - `phase` is the same shape as an example's `on.init` (`{ send, intent, do }`;
123
- see [storybook.md](./storybook.md#lifecycle-hooks-on)). `args` may be a function
124
- `(self) => [...]`, and an `intent` action takes `opts: { route: [...] }`.
125
- - An `intent` on the **`dyn`** leg has nowhere to walk under `drive`: it
126
- originates at the root, and the leg starts at the sender's *parent*. The walk
127
- runs out and the sender hears `<name>Unhandled` — which is a result you can
128
- assert on. To exercise an `intent` handler itself, call it directly.
129
- - These are *action kinds*, not methods. `$`-prefixed methods are read-only
130
- computations, not an action kind — `on`/`drive` can only reach state through
131
- `receive` / `intent` handlers. To put a component into a specific state in a
132
- unit test, call the receive recipe directly or drive a message.
133
- - `intent` actions on the `lex` leg resolve against the module's
134
- `getIntentHandlers()`.
135
- - `opts.onMessage(message, before, after)` observes every committed transaction —
136
- `message` is `{ kind, name, args, path }`, `before`/`after` are the root values
137
- around its commit — handy for asserting the message/state trace.
138
-
139
- ## Testing iteration handlers
140
-
141
- `alter` handlers run inside `@each` / `@when` / `@loop-with` /
142
- `@enrich-with` and have three distinct shapes:
143
-
144
- - `loopWith(seq, ctx)` — called once with the full collection, returns
145
- `{ iterData?, start?, end?, keys? }`: `iterData` is the shared per-loop
146
- value (defaults to `{ seq }`); `start`/`end` slice the iteration
147
- (`Array.prototype.slice` semantics, original keys preserved); `keys`
148
- is an authoritative list of original keys to visit. `this` is the
149
- parent component instance. Full return-shape and `ctx` semantics in
150
- [iteration.md](./iteration.md).
151
- - `when(key, value, iterData)` — called per element, returns truthy to
152
- keep. `this` is the parent component instance.
153
- - `enrichWith(binds, key, value, iterData)` — called per kept element;
154
- mutates `binds` (which already contains `key` and `value`). `this` is
155
- the parent component instance.
156
-
157
- You can call each one directly with `.call(comp, ...)`, but in practice
158
- you want to test them as a pipeline: filter + loop-data + enrichment
159
- together produce a list of bindings the view sees. Use
160
- `collectIterBindings` for that — a functional implementation only ships
161
- in the dev build (`tutuca/dev`); the core `tutuca` build exports a no-op
162
- stub that returns `[]`. Both commands that run `getTests()` in the
163
- terminal — `tutuca test` and `tutuca storybook` (including `--dry-run`) —
164
- redirect the bare `tutuca` import to the dev build automatically, as does
165
- the browser storybook's import map. So test modules can import it as below:
166
-
167
- ```js
168
- import { collectIterBindings } from "tutuca";
169
-
170
- const c = MyComp.make({ items: [...] });
171
- const r = collectIterBindings(MyComp, c, c.items, {
172
- loopWith: "loopHandlerName", // optional
173
- when: "whenHandlerName", // optional
174
- enrichWith: "enrichHandlerName", // optional
175
- });
176
- // r is Array<{ key, value, ...enrichments }> — one entry per kept item,
177
- // in iteration order.
178
- ```
179
-
180
- - `seq` can be a plain JS Array, a JS `Map`, or a custom collection
181
- or keyed seq.
182
- - Handler names refer to entries in `MyComp.alter`. An unknown name
183
- throws — there's no silent fallback.
184
- - The `compInstance` is `this` for every handler. Pass
185
- `MyComp.make({ field: ... })` so handlers that read `this.field` see
186
- the value you want.
187
- - The redirect uses Node's `module.register`, which is how the `tutuca`
188
- bin runs. On a runtime without loader-hook support it degrades to the
189
- no-op stub — if you see `collectIterBindings` return `[]`, import it
190
- from `tutuca/dev` explicitly.
191
-
192
- Example:
193
-
194
- ```js
195
- const Items = component({
196
- name: "Items",
197
- fields: { items: [], multiplier: 1 },
198
- alter: {
199
- loopMeta(seq) { return { iterData: { len: seq.length, doubled: seq.length * 2 } }; },
200
- keepEven(k) { return k % 2 === 0; },
201
- addLabel(binds, k, v, { len }) { binds.label = `${k}/${len}: ${v}`; },
202
- },
203
- });
204
-
205
- test("filters and enriches", () => {
206
- const c = Items.make({ items: [10, 20, 30, 40] });
207
- const r = collectIterBindings(Items, c, c.items, {
208
- loopWith: "loopMeta",
209
- when: "keepEven",
210
- enrichWith: "addLabel",
211
- });
212
- expect(r).toEqual([
213
- { key: 0, value: 10, label: "0/4: 10" },
214
- { key: 2, value: 30, label: "2/4: 30" },
215
- ]);
216
- });
217
- ```
218
-
219
- Use this whenever the iteration logic is the subject under test —
220
- no DOM, no view, no Stack/Renderer needed. For end-to-end checks that
221
- the view actually wires these handlers correctly, use
222
- `tutuca render <module>` instead.
223
-
224
- ## Designing handlers so tests stay simple
225
-
226
- Tutuca templates resolve handler args by name (see
227
- [core.md](./core.md) *Event Handling*). When you author a handler,
228
- **pick the most specific named args you need; don't take the raw
229
- event**. With named args, the test passes a literal; with `event`,
230
- the test must fabricate a DOM-event-shaped object.
231
-
232
- An event always names a `receive` handler without a prefix. `$method` is for
233
- read-only value slots and is rejected in `@on.*`. What matters for testability
234
- is which named argument the receive handler asks for.
235
-
236
- **Bad — receive handler taking the whole event:**
237
-
238
- ```html
239
- <input @on.input="setName event" />
240
- ```
241
- ```js
242
- receive: { setName(draft, event) { draft.name = event.target.value; } }
243
- ```
244
-
245
- **Good — receive handler taking the value:**
246
-
247
- ```html
248
- <input @on.input="setName value" />
249
- ```
250
- ```js
251
- receive: { setName(draft, value) { draft.name = value; } }
252
- ```
253
-
254
- **Bad — receive handler:**
255
-
256
- ```html
257
- <input @on.input="setCount event" />
258
- ```
259
- ```js
260
- receive: { setCount(draft, event) { draft.count = parseInt(event.target.value, 10); } }
261
- ```
262
-
263
- **Good — receive handler:**
264
-
265
- ```html
266
- <input @on.input="setCount valueAsInt" />
267
- ```
268
- ```js
269
- receive: { setCount(draft, n) { draft.count = n; } }
270
- ```
271
-
272
- At test time, the "good" forms become trivial:
273
-
274
- ```js
275
- const value = MyComp.make();
276
- expect(produce(value, (draft) => value.setName(draft, "Ada")).name).toBe("Ada");
277
- expect(produce(value, (draft) => MyComp.receive.setCount.call(value, draft, 42)).count).toBe(42);
278
- ```
279
-
280
- The "bad" forms force every test to construct
281
- `{ target: { value: "42" } }` (or a fuller stub when more fields are
282
- read), which is brittle and obscures intent.
283
-
284
- The built-in named args are listed in [core.md](./core.md) *Event
285
- Handling*; `ctx` is auto-appended last. Reach for `event` only when no
286
- narrower arg fits.
287
-
288
- ## Worked example
289
-
290
- A `getTests` export covering two receive handlers (`inc` and `dec`) and a
291
- receive handler with a named arg (`setCount` taking
292
- `valueAsInt`):
293
-
294
- ```js
295
- import { produce } from "tutuca/immer";
296
-
297
- export function getTests({ describe, test, expect }) {
298
- describe(Counter, () => {
299
- describe("inc", () => { // receive handler
300
- test("returns a Counter with count + 1", () => {
301
- const c = Counter.make();
302
- expect(produce(c, (draft) => Counter.receive.inc.call(c, draft)).count).toBe(1);
303
- });
304
- test("does not mutate the original instance", () => {
305
- const c = Counter.make({ count: 7 });
306
- produce(c, (draft) => Counter.receive.inc.call(c, draft));
307
- expect(c.count).toBe(7);
308
- });
309
- });
310
-
311
- describe("dec()", () => { // receive handler, no args
312
- test("returns a Counter with count - 1", () => {
313
- const c = Counter.make();
314
- const next = produce(c, (draft) => Counter.receive.dec.call(c, draft));
315
- expect(next.count).toBe(-1);
316
- });
317
- });
318
-
319
- describe("setCount()", () => { // receive handler, valueAsInt
320
- test("sets the count from a parsed int", () => {
321
- const c = Counter.make();
322
- const next = produce(c, (draft) => Counter.receive.setCount.call(c, draft, 42));
323
- expect(next.count).toBe(42);
324
- });
325
- });
326
-
327
- test("inc and dec round-trip", () => { // untagged, inherits Counter
328
- const c = Counter.make();
329
- const next = produce(c, (draft) => {
330
- Counter.receive.inc.call(c, draft);
331
- Counter.receive.dec.call(c, draft);
332
- });
333
- expect(next.count).toBe(0);
334
- });
335
- });
336
- }
337
- ```
338
-
339
- ## See also
340
-
341
- - [core.md](./core.md) — *Verifying changes*, *Event Handling*,
342
- *Component Skeleton*.
343
- - [messages-and-intents.md](./messages-and-intents.md) — handler signatures for
344
- `receive` / `intent`, routes and the three outcomes, `$unknown`.
345
- - [cli.md](./cli.md) — `test` flags, exit codes, output formats,
346
- `--grep` syntax.