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.
- package/index.js +12 -10
- package/package.json +2 -2
- package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-drawing/SKILL.md +89 -0
- package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
- package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
- package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
- package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
- package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
- package/template/AGENTS.md +34 -27
- package/template/README.md +19 -18
- package/template/_gitignore +1 -0
- package/template/src/App.tsx +16 -13
- package/template/src/components/Activity.tsx +24 -0
- package/template/src/components/Counter.tsx +24 -0
- package/template/src/components/Tasks.tsx +54 -0
- package/template/src/icons.ts +10 -0
- package/template/src/state.ts +50 -0
- package/template/test/App.test.tsx +45 -35
- package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
- package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
- package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
- package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
- package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
- package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
- package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
- package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
- package/template/src/componentes/Atividade.tsx +0 -24
- package/template/src/componentes/Contador.tsx +0 -22
- package/template/src/componentes/Tarefas.tsx +0 -38
- package/template/src/estado.ts +0 -50
- 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.
|