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.
- package/dist/tutuca-cli.js +264 -361
- package/dist/tutuca-components.js +175 -175
- package/dist/tutuca-dev.ext.js +250 -323
- package/dist/tutuca-dev.js +250 -323
- package/dist/tutuca-dev.min.js +4 -4
- package/dist/tutuca-extra.ext.js +55 -46
- package/dist/tutuca-extra.js +55 -46
- package/dist/tutuca-extra.min.js +3 -3
- package/dist/tutuca-storybook.js +29 -29
- package/dist/tutuca.ext.js +55 -46
- package/dist/tutuca.js +55 -46
- package/dist/tutuca.min.js +3 -3
- package/package.json +4 -3
- package/skill/margaui/SKILL.md +0 -105
- package/skill/margaui/components/accordion.md +0 -127
- package/skill/margaui/components/alert.md +0 -174
- package/skill/margaui/components/aura.md +0 -97
- package/skill/margaui/components/avatar.md +0 -220
- package/skill/margaui/components/badge.md +0 -193
- package/skill/margaui/components/breadcrumbs.md +0 -103
- package/skill/margaui/components/button.md +0 -322
- package/skill/margaui/components/calendar.md +0 -67
- package/skill/margaui/components/card.md +0 -373
- package/skill/margaui/components/carousel.md +0 -387
- package/skill/margaui/components/chat.md +0 -171
- package/skill/margaui/components/checkbox.md +0 -101
- package/skill/margaui/components/collapse.md +0 -172
- package/skill/margaui/components/countdown.md +0 -165
- package/skill/margaui/components/diff.md +0 -53
- package/skill/margaui/components/divider.md +0 -107
- package/skill/margaui/components/dock.md +0 -173
- package/skill/margaui/components/drawer.md +0 -184
- package/skill/margaui/components/dropdown.md +0 -388
- package/skill/margaui/components/fab.md +0 -346
- package/skill/margaui/components/fieldset.md +0 -88
- package/skill/margaui/components/file-input.md +0 -84
- package/skill/margaui/components/filter.md +0 -52
- package/skill/margaui/components/footer.md +0 -583
- package/skill/margaui/components/hero.md +0 -135
- package/skill/margaui/components/hover-3d.md +0 -129
- package/skill/margaui/components/hover-gallery.md +0 -49
- package/skill/margaui/components/indicator.md +0 -265
- package/skill/margaui/components/input.md +0 -389
- package/skill/margaui/components/join.md +0 -100
- package/skill/margaui/components/kbd.md +0 -127
- package/skill/margaui/components/label.md +0 -102
- package/skill/margaui/components/link.md +0 -96
- package/skill/margaui/components/list.md +0 -182
- package/skill/margaui/components/loading.md +0 -105
- package/skill/margaui/components/mask.md +0 -168
- package/skill/margaui/components/megamenu.md +0 -131
- package/skill/margaui/components/menu.md +0 -887
- package/skill/margaui/components/mockup-browser.md +0 -39
- package/skill/margaui/components/mockup-code.md +0 -81
- package/skill/margaui/components/mockup-phone.md +0 -39
- package/skill/margaui/components/mockup-window.md +0 -33
- package/skill/margaui/components/modal.md +0 -196
- package/skill/margaui/components/navbar.md +0 -282
- package/skill/margaui/components/otp.md +0 -171
- package/skill/margaui/components/pagination.md +0 -122
- package/skill/margaui/components/progress.md +0 -135
- package/skill/margaui/components/radial-progress.md +0 -67
- package/skill/margaui/components/radio.md +0 -133
- package/skill/margaui/components/range.md +0 -134
- package/skill/margaui/components/rating.md +0 -170
- package/skill/margaui/components/select.md +0 -225
- package/skill/margaui/components/skeleton.md +0 -64
- package/skill/margaui/components/stack.md +0 -142
- package/skill/margaui/components/stat.md +0 -254
- package/skill/margaui/components/status.md +0 -73
- package/skill/margaui/components/steps.md +0 -138
- package/skill/margaui/components/swap.md +0 -152
- package/skill/margaui/components/tab.md +0 -248
- package/skill/margaui/components/table.md +0 -1018
- package/skill/margaui/components/text-rotate.md +0 -91
- package/skill/margaui/components/textarea.md +0 -85
- package/skill/margaui/components/theme-controller.md +0 -266
- package/skill/margaui/components/timeline.md +0 -1356
- package/skill/margaui/components/toast.md +0 -165
- package/skill/margaui/components/toggle.md +0 -135
- package/skill/margaui/components/tooltip.md +0 -181
- package/skill/margaui/components/validator.md +0 -163
- package/skill/tutuca/SKILL.md +0 -56
- package/skill/tutuca/advanced.md +0 -212
- package/skill/tutuca/cli.md +0 -239
- package/skill/tutuca/component-design.md +0 -168
- package/skill/tutuca/core.md +0 -908
- package/skill/tutuca/iteration.md +0 -207
- package/skill/tutuca/macros.md +0 -86
- package/skill/tutuca/margaui.md +0 -175
- package/skill/tutuca/messages-and-intents.md +0 -399
- package/skill/tutuca/patterns/README.md +0 -48
- package/skill/tutuca/patterns/add-a-story.md +0 -26
- package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
- package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
- package/skill/tutuca/patterns/coordinate-components.md +0 -54
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -27
- package/skill/tutuca/patterns/enrich-each-item.md +0 -25
- package/skill/tutuca/patterns/file-input.md +0 -39
- package/skill/tutuca/patterns/filter-a-list.md +0 -25
- package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
- package/skill/tutuca/patterns/handle-events.md +0 -38
- package/skill/tutuca/patterns/iterate-a-list.md +0 -18
- package/skill/tutuca/patterns/paginate-a-list.md +0 -29
- package/skill/tutuca/patterns/render-a-child-component.md +0 -21
- package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
- package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -38
- package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
- package/skill/tutuca/patterns/switch-between-views.md +0 -30
- package/skill/tutuca/patterns/tabbed-interface.md +0 -43
- package/skill/tutuca/semantics.md +0 -195
- package/skill/tutuca/storybook.md +0 -270
- package/skill/tutuca/styles.md +0 -48
- package/skill/tutuca/testing.md +0 -346
- package/skill/tutuca-source/SKILL.md +0 -33
- 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.
|
package/skill/tutuca/styles.md
DELETED
|
@@ -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).
|
package/skill/tutuca/testing.md
DELETED
|
@@ -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.
|