create-nodebuilderjs 0.2.1 → 0.3.0

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 (34) hide show
  1. package/index.js +12 -10
  2. package/package.json +2 -2
  3. package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
  4. package/template/.agents/skills/nodebuilder-drawing/SKILL.md +89 -0
  5. package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
  6. package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
  7. package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
  8. package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
  9. package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
  10. package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
  11. package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
  12. package/template/AGENTS.md +34 -27
  13. package/template/README.md +19 -18
  14. package/template/_gitignore +1 -0
  15. package/template/src/App.tsx +16 -13
  16. package/template/src/components/Activity.tsx +24 -0
  17. package/template/src/components/Counter.tsx +24 -0
  18. package/template/src/components/Tasks.tsx +54 -0
  19. package/template/src/icons.ts +10 -0
  20. package/template/src/state.ts +50 -0
  21. package/template/test/App.test.tsx +45 -35
  22. package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
  23. package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
  24. package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
  25. package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
  26. package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
  27. package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
  28. package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
  29. package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
  30. package/template/src/componentes/Atividade.tsx +0 -24
  31. package/template/src/componentes/Contador.tsx +0 -22
  32. package/template/src/componentes/Tarefas.tsx +0 -38
  33. package/template/src/estado.ts +0 -50
  34. package/template/src/icones.ts +0 -11
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: nodebuilder-testing
3
+ description: Write and debug tests for nodebuilder apps with bun test, without opening a window (mount, findByText, findByLabel, click, interact for mouse and keyboard, layout, debugTree, layoutWarnings for hidden overflow, real text metrics), and look at the UI with screenshots (nodebuilder screenshot, screenshot()). Use when creating tests, when verifying a behavior, layout or visual change, or when a component test fails.
4
+ ---
5
+
6
+ # Tests without a window
7
+
8
+ `bun test` runs the files in `test/`. Nothing opens a window: `mount` builds the tree with
9
+ fake text metrics (**8 px per character, 20 px line**), so layout is deterministic (real
10
+ fonts on request: `useRealTextMetrics()`, below).
11
+
12
+ ```tsx
13
+ import { expect, test } from "bun:test";
14
+ import { click, findAll, findByText, mount } from "@nodebuilderjs/core/testing";
15
+ import { debugTree } from "@nodebuilderjs/core";
16
+ import { App } from "../src/App.js";
17
+
18
+ test("adds a task", () => {
19
+ const { root, relayout, dispose } = mount(() => <App />, { width: 800, height: 600 });
20
+ click(findByText(root, "Add")!); // walks up to the parent with onClick (the Button)
21
+ expect(findByText(root, /1 task/)).toBeDefined();
22
+ relayout(); // recomputes positions after changes
23
+ const item = findByText(root, "New task")!;
24
+ expect(item.y).toBeGreaterThan(0);
25
+ dispose(); // disposes the app's effects and signals
26
+ });
27
+ ```
28
+
29
+ ## API (`@nodebuilderjs/core/testing`)
30
+
31
+ - `mount(view, size?)` → `{ root, relayout, dispose, layoutWarnings }`. Layout runs once in
32
+ `mount`; call `relayout()` before checking `x`, `y`, `width`, `height` after changing state.
33
+ - `layoutWarnings()` (from `mount`, or `layoutWarnings(root)`): overflow warnings of the last
34
+ layout, e.g. `layout: Row > Text("Artists") overflows Box(panel) by 42px (children need
35
+ 342px, 300px available)`: children that need more room than they get, cut off by a `clip`
36
+ ancestor or the window (past a `Scroll` on its scrolling axis does not count). `mount` does
37
+ not fail on them; assert `expect(layoutWarnings()).toEqual([])` in screen tests so a hidden
38
+ chip fails right away instead of when a click finds it.
39
+ - `findByText(root, "text" | /regex/)`: first `Text` whose entire content matches;
40
+ `findAllByText` returns every match (e.g. a title repeated in a list and in a player bar).
41
+ - `findByLabel(root, "label" | /regex/)` / `findAllByLabel`: by `ariaLabel`, for elements
42
+ without text (`<Button ariaLabel="Play">` with only an icon). Built-in labels: the window's
43
+ title-bar buttons (`"Minimize"`, `"Maximize"` or `"Restore"`, `"Close"`), and a `Tooltip`'s
44
+ trigger gets the tip text when it has no `ariaLabel` of its own.
45
+ - `findAll(root, (n) => ...)`: free-form search; `n.label` is `"Text \"...\""`, `"Button"`, `"Row"`...
46
+ - `click(node)`: fires the `onClick` of the node or of its nearest ancestor (quick shortcut),
47
+ at the node's center.
48
+ - `interact(root)`: mouse and keyboard delivered as in the real app (hover, focus, capture
49
+ while dragging, Tab): `hover(n)`, `press(n)`, `release(n)`, `drag(n, dx, dy)`, `move(x, y)`,
50
+ `down(x, y)`/`up(x, y)` (button at a window position, e.g. on the scrollbar),
51
+ `scroll(n, dy, { dx, shift })`, `key("Return")`, `key("c", { ctrl: true })`,
52
+ `type("text")`, `focused` and `cursor` (the cursor the window would show).
53
+
54
+ ```tsx
55
+ const ui = interact(root);
56
+ ui.press(field); // focuses the TextField
57
+ ui.type("Ana"); // types into the focused field
58
+ ui.key("Return"); // fires onSubmit
59
+ ui.drag(slider, 80, 0); // drags 80 px to the right
60
+ ```
61
+ - **Hidden targets throw.** `click(n)` and the node helpers of `interact` (`hover`, `press`,
62
+ `release`, `drag`, `scroll`) throw when a user could not reach the node: it is `culled`
63
+ (it, or an ancestor, is outside the visible area of the last layout, e.g. a chip that
64
+ overflowed a `clip` panel or a row far down a `Scroll`), or its center is cut off by a
65
+ clipping/scrolling ancestor or the window. The message names the node and the ancestor.
66
+ Fix the test (or the layout): `n.scrollIntoView()` then `relayout()`, give it room (a
67
+ bigger `mount` size, `width`), or `relayout()` if the tree changed since the last layout.
68
+ `down(x, y)`/`up(x, y)`/`move(x, y)` take window positions and are never checked.
69
+
70
+ ```tsx
71
+ import { expect, test } from "bun:test";
72
+ import { Button, For, Row, Scroll } from "@nodebuilderjs/core";
73
+ import { click, findAllByText, findByLabel, findByText, mount } from "@nodebuilderjs/core/testing";
74
+
75
+ test("plays a genre that starts off screen", () => {
76
+ const played: string[] = [];
77
+ const genres = ["Rock", "Jazz", "Pop", "Funk", "Soul"];
78
+ const { root, relayout } = mount(() => (
79
+ <Row>
80
+ <Button ariaLabel="Play" label="▶" onClick={() => played.push("all")} />
81
+ <Scroll horizontal width={300}>
82
+ <For each={genres}>{(g) => <Button label={g} width={100} onClick={() => played.push(g)} />}</For>
83
+ </Scroll>
84
+ </Row>
85
+ ));
86
+ click(findByLabel(root, "Play")!);
87
+ const soul = findByText(root, "Soul")!;
88
+ expect(() => click(soul)).toThrow(/culled/); // past the 300 px Scroll
89
+ soul.scrollIntoView();
90
+ relayout();
91
+ click(soul);
92
+ expect(played).toEqual(["all", "Soul"]);
93
+ expect(findAllByText(root, /o/)).toHaveLength(3); // Rock, Pop, Soul
94
+ });
95
+ ```
96
+ - **Real text metrics:** `useRealTextMetrics()` makes this and every later `mount` measure
97
+ text with the installed fonts (no window) and returns a function that restores the previous
98
+ metrics; `useFakeTextMetrics({ charWidth, lineHeight })` sets fake ones (the default is 8
99
+ and 20). Use real metrics to check that a label really fits (`layoutWarnings()` empty with
100
+ a `clip` panel); numbers depend on the machine's fonts, so assert relations, not exact
101
+ pixels. Guard with `test.skipIf(!canUseRealTextMetrics())` and restore in `afterEach`.
102
+
103
+ ```tsx
104
+ import { afterEach, expect, test } from "bun:test";
105
+ import { Row, Text } from "@nodebuilderjs/core";
106
+ import { canUseRealTextMetrics, layoutWarnings, mount, useFakeTextMetrics, useRealTextMetrics } from "@nodebuilderjs/core/testing";
107
+
108
+ afterEach(() => useFakeTextMetrics());
109
+
110
+ test.skipIf(!canUseRealTextMetrics())("the header fits with real fonts", () => {
111
+ useRealTextMetrics();
112
+ const { root } = mount(() => <Row width={240} clip><Text>Recently played</Text></Row>);
113
+ expect(layoutWarnings(root)).toEqual([]);
114
+ });
115
+ ```
116
+ - `debugTree(node)` (from `@nodebuilderjs/core`): tree with positions, great for inspecting
117
+ layout; a container whose children overflow a clip is marked `[overflow +42]`.
118
+ - Animations finish immediately in tests (there is no window): a closed `Dialog` leaves the
119
+ tree at once. To test the animation, `const clock = frames()` after `mount()` and
120
+ `clock.advance(ms)` (16 ms frames).
121
+ - `ref={(el) => el.focus()}` (focus on creation) takes effect in a microtask: create
122
+ `interact(root)` and `await Promise.resolve()` before typing. `onResize` fires on every
123
+ `relayout()` in which the size changed.
124
+ - Scrolling: `n.scrollY`/`n.scrollX` give the position; `ui.scroll(n, 60)` uses the wheel,
125
+ `ui.key("Next")` (PageDown; PageUp is `"Prior"`) the keyboard. `scrollTo` and `scrollIntoView()`
126
+ apply on the next layout: call `relayout()` before checking.
127
+ - Overlays are positioned during layout: after opening or closing one, call `relayout()`.
128
+ Their content lives in `root.layers` (not in `root.children`); `findByText(root, ...)` searches
129
+ both.
130
+
131
+ ## Look at the UI: screenshots
132
+
133
+ Tests check boxes, not looks. **After a visual change, take a screenshot and look at it**
134
+ (read the PNG): wrong colors, clipped text, misalignment and missing icons show up at once.
135
+ No window or display server is needed (it renders with the GPU through EGL):
136
+
137
+ ```sh
138
+ bun run screenshot # screenshot.png: src/main.tsx at the <Window>'s size
139
+ nodebuilder screenshot src/main.tsx dark.png --theme dark --scale 2 --width 1024 --height 700
140
+ ```
141
+
142
+ Animations run to their end on a simulated clock (`--wait-frames <n>` stops earlier). In a
143
+ test, `screenshot()` gives the real pixels of a mounted tree, with real fonts:
144
+
145
+ ```tsx
146
+ import { expect, test } from "bun:test";
147
+ import { Box } from "@nodebuilderjs/core";
148
+ import { canScreenshot, mount, savePNG, screenshot } from "@nodebuilderjs/core/testing";
149
+
150
+ test.skipIf(!canScreenshot())("the banner is red", () => {
151
+ const { root } = mount(() => <Box height={40} background="#e01b24" />, { width: 200, height: 40 });
152
+ const shot = screenshot(root, { scale: 2 }); // 400x80 pixels
153
+ expect(shot.pixel(10, 10)).toEqual([0xe0, 0x1b, 0x24, 255]); // [r, g, b, a], straight alpha
154
+ savePNG(shot, "banner.png"); // to look at it
155
+ });
156
+ ```
157
+
158
+ - `screenshot(root | () => view, { width, height, scale, background })` →
159
+ `{ width, height, scale, rgba, pixel(x, y), toPNG() }`; size in device pixels. Uncovered
160
+ areas stay transparent. The test's text metrics (fake by default) come back after the shot.
161
+ - Pixels depend on the machine's fonts and GPU: assert colors at chosen points, not whole
162
+ images. `canScreenshot()` is false without a GPU driver (e.g. some CI runners).
163
+
164
+ ## Clipboard and selection
165
+
166
+ In tests, `clipboard` is an in-memory area (no window, no system). To check a copy, read it
167
+ back; reading is asynchronous, so the test becomes `async`:
168
+
169
+ ```tsx
170
+ import { expect, test } from "bun:test";
171
+ import { Column, Text, clipboard } from "@nodebuilderjs/core";
172
+ import { interact, mount } from "@nodebuilderjs/core/testing";
173
+
174
+ test("copies the id by selecting the text", async () => {
175
+ const { root } = mount(() => <Column align="start"><Text selectable>id-42</Text></Column>);
176
+ const ui = interact(root);
177
+ const id = root.children[0]!.children[0]!;
178
+ ui.press(id); // the click gives focus to the text
179
+ ui.release(id);
180
+ ui.key("a", { ctrl: true }); // Ctrl+A: everything (or ui.drag(id, dx, 0) for a range)
181
+ ui.key("c", { ctrl: true });
182
+ expect(await clipboard.readText()).toBe("id-42");
183
+ });
184
+ ```
185
+
186
+ To test pasting into a `TextField`, write first with `clipboard.writeText(...)`, focus the
187
+ field, `ui.key("v", { ctrl: true })` and wait one loop turn
188
+ (`await new Promise((r) => setImmediate(r))`) before checking the value. The in-memory area
189
+ is shared within the file: write the expected value at the start of each test.
190
+
191
+ ## Tips
192
+
193
+ - State changes immediately (signals are synchronous); there is no `await` or `act()` (the
194
+ exception is reading the clipboard, above).
195
+ - Prefer `interact` over calling handlers by hand: it also tests focus, hover and propagation.
196
+ After changes that move nodes, call `relayout()` before interacting again.
197
+ - Test layout calculations with fixed sizes (`width`, `height`) so they do not depend on the
198
+ fake metrics.
199
+ - Before finishing a task: `bun run typecheck && bun test`, and a screenshot if anything visual changed.