@dealdeploy/skl 0.1.7 → 0.2.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/.agents/skills/opentui/SKILL.md +198 -0
- package/.agents/skills/opentui/references/animation/REFERENCE.md +431 -0
- package/.agents/skills/opentui/references/components/REFERENCE.md +143 -0
- package/.agents/skills/opentui/references/components/code-diff.md +496 -0
- package/.agents/skills/opentui/references/components/containers.md +412 -0
- package/.agents/skills/opentui/references/components/inputs.md +531 -0
- package/.agents/skills/opentui/references/components/text-display.md +384 -0
- package/.agents/skills/opentui/references/core/REFERENCE.md +145 -0
- package/.agents/skills/opentui/references/core/api.md +506 -0
- package/.agents/skills/opentui/references/core/configuration.md +166 -0
- package/.agents/skills/opentui/references/core/gotchas.md +393 -0
- package/.agents/skills/opentui/references/core/patterns.md +448 -0
- package/.agents/skills/opentui/references/keyboard/REFERENCE.md +511 -0
- package/.agents/skills/opentui/references/layout/REFERENCE.md +337 -0
- package/.agents/skills/opentui/references/layout/patterns.md +444 -0
- package/.agents/skills/opentui/references/react/REFERENCE.md +174 -0
- package/.agents/skills/opentui/references/react/api.md +435 -0
- package/.agents/skills/opentui/references/react/configuration.md +301 -0
- package/.agents/skills/opentui/references/react/gotchas.md +443 -0
- package/.agents/skills/opentui/references/react/patterns.md +501 -0
- package/.agents/skills/opentui/references/solid/REFERENCE.md +201 -0
- package/.agents/skills/opentui/references/solid/api.md +543 -0
- package/.agents/skills/opentui/references/solid/configuration.md +315 -0
- package/.agents/skills/opentui/references/solid/gotchas.md +415 -0
- package/.agents/skills/opentui/references/solid/patterns.md +558 -0
- package/.agents/skills/opentui/references/testing/REFERENCE.md +614 -0
- package/.claude/skills/opentui/SKILL.md +198 -0
- package/.claude/skills/opentui/references/animation/REFERENCE.md +431 -0
- package/.claude/skills/opentui/references/components/REFERENCE.md +143 -0
- package/.claude/skills/opentui/references/components/code-diff.md +496 -0
- package/.claude/skills/opentui/references/components/containers.md +412 -0
- package/.claude/skills/opentui/references/components/inputs.md +531 -0
- package/.claude/skills/opentui/references/components/text-display.md +384 -0
- package/.claude/skills/opentui/references/core/REFERENCE.md +145 -0
- package/.claude/skills/opentui/references/core/api.md +506 -0
- package/.claude/skills/opentui/references/core/configuration.md +166 -0
- package/.claude/skills/opentui/references/core/gotchas.md +393 -0
- package/.claude/skills/opentui/references/core/patterns.md +448 -0
- package/.claude/skills/opentui/references/keyboard/REFERENCE.md +511 -0
- package/.claude/skills/opentui/references/layout/REFERENCE.md +337 -0
- package/.claude/skills/opentui/references/layout/patterns.md +444 -0
- package/.claude/skills/opentui/references/react/REFERENCE.md +174 -0
- package/.claude/skills/opentui/references/react/api.md +435 -0
- package/.claude/skills/opentui/references/react/configuration.md +301 -0
- package/.claude/skills/opentui/references/react/gotchas.md +443 -0
- package/.claude/skills/opentui/references/react/patterns.md +501 -0
- package/.claude/skills/opentui/references/solid/REFERENCE.md +201 -0
- package/.claude/skills/opentui/references/solid/api.md +543 -0
- package/.claude/skills/opentui/references/solid/configuration.md +315 -0
- package/.claude/skills/opentui/references/solid/gotchas.md +415 -0
- package/.claude/skills/opentui/references/solid/patterns.md +558 -0
- package/.claude/skills/opentui/references/testing/REFERENCE.md +614 -0
- package/index.ts +169 -38
- package/package.json +1 -1
- package/update.ts +87 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# Testing OpenTUI Applications
|
|
2
|
+
|
|
3
|
+
How to test terminal user interfaces built with OpenTUI.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
OpenTUI provides:
|
|
8
|
+
- **Test Renderer**: Headless renderer for testing
|
|
9
|
+
- **Snapshot Testing**: Verify visual output
|
|
10
|
+
- **Interaction Testing**: Simulate user input
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
Use this reference when you need snapshot tests, interaction testing, or renderer-based regression checks.
|
|
15
|
+
|
|
16
|
+
## Test Setup
|
|
17
|
+
|
|
18
|
+
### Bun Test Runner
|
|
19
|
+
|
|
20
|
+
OpenTUI uses Bun's built-in test runner:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { test, expect, beforeEach, afterEach } from "bun:test"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Test Renderer
|
|
27
|
+
|
|
28
|
+
Create a test renderer for headless testing:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createTestRenderer } from "@opentui/core/testing"
|
|
32
|
+
|
|
33
|
+
const testSetup = await createTestRenderer({
|
|
34
|
+
width: 80, // Terminal width
|
|
35
|
+
height: 24, // Terminal height
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Core Testing
|
|
40
|
+
|
|
41
|
+
### Basic Test
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { test, expect } from "bun:test"
|
|
45
|
+
import { createTestRenderer } from "@opentui/core/testing"
|
|
46
|
+
import { TextRenderable } from "@opentui/core"
|
|
47
|
+
|
|
48
|
+
test("renders text", async () => {
|
|
49
|
+
const testSetup = await createTestRenderer({
|
|
50
|
+
width: 40,
|
|
51
|
+
height: 10,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const text = new TextRenderable(testSetup.renderer, {
|
|
55
|
+
id: "greeting",
|
|
56
|
+
content: "Hello, World!",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
testSetup.renderer.root.add(text)
|
|
60
|
+
await testSetup.renderOnce()
|
|
61
|
+
|
|
62
|
+
expect(testSetup.captureCharFrame()).toContain("Hello, World!")
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Snapshot Testing
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { test, expect, afterEach } from "bun:test"
|
|
70
|
+
import { createTestRenderer } from "@opentui/core/testing"
|
|
71
|
+
import { BoxRenderable, TextRenderable } from "@opentui/core"
|
|
72
|
+
|
|
73
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
if (testSetup) {
|
|
77
|
+
testSetup.renderer.destroy()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("component matches snapshot", async () => {
|
|
82
|
+
testSetup = await createTestRenderer({
|
|
83
|
+
width: 40,
|
|
84
|
+
height: 10,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const box = new BoxRenderable(testSetup.renderer, {
|
|
88
|
+
id: "box",
|
|
89
|
+
border: true,
|
|
90
|
+
width: 20,
|
|
91
|
+
height: 5,
|
|
92
|
+
})
|
|
93
|
+
box.add(new TextRenderable(testSetup.renderer, {
|
|
94
|
+
content: "Content",
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
testSetup.renderer.root.add(box)
|
|
98
|
+
await testSetup.renderOnce()
|
|
99
|
+
|
|
100
|
+
expect(testSetup.captureCharFrame()).toMatchSnapshot()
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## React Testing
|
|
105
|
+
|
|
106
|
+
### Test Utilities
|
|
107
|
+
|
|
108
|
+
React provides a built-in `testRender` utility via the `@opentui/react/test-utils` subpath export:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This utility:
|
|
115
|
+
- Creates a headless test renderer
|
|
116
|
+
- Sets up the React Act environment automatically
|
|
117
|
+
- Handles proper unmounting on destroy
|
|
118
|
+
- Returns the standard test setup object
|
|
119
|
+
|
|
120
|
+
### Basic Component Test
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { test, expect } from "bun:test"
|
|
124
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
125
|
+
|
|
126
|
+
function Greeting({ name }: { name: string }) {
|
|
127
|
+
return <text>Hello, {name}!</text>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
test("Greeting renders name", async () => {
|
|
131
|
+
const testSetup = await testRender(
|
|
132
|
+
<Greeting name="World" />,
|
|
133
|
+
{ width: 80, height: 24 }
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await testSetup.renderOnce()
|
|
137
|
+
const frame = testSetup.captureCharFrame()
|
|
138
|
+
|
|
139
|
+
expect(frame).toContain("Hello, World!")
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Snapshot Testing
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { test, expect, afterEach } from "bun:test"
|
|
147
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
148
|
+
|
|
149
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
150
|
+
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
if (testSetup) {
|
|
153
|
+
testSetup.renderer.destroy()
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test("component matches snapshot", async () => {
|
|
158
|
+
testSetup = await testRender(
|
|
159
|
+
<box style={{ width: 20, height: 5, border: true }}>
|
|
160
|
+
<text>Content</text>
|
|
161
|
+
</box>,
|
|
162
|
+
{ width: 25, height: 8 }
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
await testSetup.renderOnce()
|
|
166
|
+
const frame = testSetup.captureCharFrame()
|
|
167
|
+
|
|
168
|
+
expect(frame).toMatchSnapshot()
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### State Testing
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { test, expect, afterEach } from "bun:test"
|
|
176
|
+
import { useState } from "react"
|
|
177
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
178
|
+
|
|
179
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
180
|
+
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
if (testSetup) {
|
|
183
|
+
testSetup.renderer.destroy()
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
function Counter() {
|
|
188
|
+
const [count, setCount] = useState(0)
|
|
189
|
+
return (
|
|
190
|
+
<box>
|
|
191
|
+
<text>Count: {count}</text>
|
|
192
|
+
</box>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test("Counter shows initial value", async () => {
|
|
197
|
+
testSetup = await testRender(
|
|
198
|
+
<Counter />,
|
|
199
|
+
{ width: 20, height: 5 }
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
await testSetup.renderOnce()
|
|
203
|
+
const frame = testSetup.captureCharFrame()
|
|
204
|
+
|
|
205
|
+
expect(frame).toContain("Count: 0")
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Test Setup/Teardown Pattern
|
|
210
|
+
|
|
211
|
+
For multiple tests, use beforeEach/afterEach to manage the renderer lifecycle:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
|
215
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
216
|
+
|
|
217
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
218
|
+
|
|
219
|
+
describe("MyComponent", () => {
|
|
220
|
+
beforeEach(async () => {
|
|
221
|
+
if (testSetup) {
|
|
222
|
+
testSetup.renderer.destroy()
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
afterEach(() => {
|
|
227
|
+
if (testSetup) {
|
|
228
|
+
testSetup.renderer.destroy()
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test("renders correctly", async () => {
|
|
233
|
+
testSetup = await testRender(<MyComponent />, {
|
|
234
|
+
width: 40,
|
|
235
|
+
height: 10,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
await testSetup.renderOnce()
|
|
239
|
+
const frame = testSetup.captureCharFrame()
|
|
240
|
+
expect(frame).toMatchSnapshot()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Test Setup Return Object
|
|
246
|
+
|
|
247
|
+
The `testRender` function returns a test setup object with these properties:
|
|
248
|
+
|
|
249
|
+
| Property | Type | Description |
|
|
250
|
+
|----------|------|-------------|
|
|
251
|
+
| `renderer` | `Renderer` | The headless renderer instance |
|
|
252
|
+
| `renderOnce` | `() => Promise<void>` | Triggers a single render cycle |
|
|
253
|
+
| `captureCharFrame` | `() => string` | Captures current output as text |
|
|
254
|
+
| `resize` | `(width, height) => void` | Resize the virtual terminal |
|
|
255
|
+
|
|
256
|
+
## Solid Testing
|
|
257
|
+
|
|
258
|
+
### Test Utilities
|
|
259
|
+
|
|
260
|
+
Solid exports `testRender` directly from the main package:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { testRender } from "@opentui/solid"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Note: Unlike React, Solid's `testRender` takes a **function component** (not a JSX element).
|
|
267
|
+
|
|
268
|
+
### Basic Component Test
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { test, expect } from "bun:test"
|
|
272
|
+
import { testRender } from "@opentui/solid"
|
|
273
|
+
|
|
274
|
+
function Greeting(props: { name: string }) {
|
|
275
|
+
return <text>Hello, {props.name}!</text>
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
test("Greeting renders name", async () => {
|
|
279
|
+
const testSetup = await testRender(
|
|
280
|
+
() => <Greeting name="World" />,
|
|
281
|
+
{ width: 80, height: 24 }
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
await testSetup.renderOnce()
|
|
285
|
+
const frame = testSetup.captureCharFrame()
|
|
286
|
+
|
|
287
|
+
expect(frame).toContain("Hello, World!")
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Snapshot Testing
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { test, expect, afterEach } from "bun:test"
|
|
295
|
+
import { testRender } from "@opentui/solid"
|
|
296
|
+
|
|
297
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
298
|
+
|
|
299
|
+
afterEach(() => {
|
|
300
|
+
if (testSetup) {
|
|
301
|
+
testSetup.renderer.destroy()
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test("component matches snapshot", async () => {
|
|
306
|
+
testSetup = await testRender(
|
|
307
|
+
() => (
|
|
308
|
+
<box style={{ width: 20, height: 5, border: true }}>
|
|
309
|
+
<text>Content</text>
|
|
310
|
+
</box>
|
|
311
|
+
),
|
|
312
|
+
{ width: 25, height: 8 }
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
await testSetup.renderOnce()
|
|
316
|
+
const frame = testSetup.captureCharFrame()
|
|
317
|
+
|
|
318
|
+
expect(frame).toMatchSnapshot()
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Snapshot Format
|
|
323
|
+
|
|
324
|
+
Snapshots capture the rendered terminal output as text:
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
┌──────────────────┐
|
|
328
|
+
│ Hello, World! │
|
|
329
|
+
│ │
|
|
330
|
+
└──────────────────┘
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Updating Snapshots
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
bun test --update-snapshots
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Interaction Testing
|
|
340
|
+
|
|
341
|
+
### Simulating Key Presses
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { test, expect, afterEach } from "bun:test"
|
|
345
|
+
import { createTestRenderer } from "@opentui/core/testing"
|
|
346
|
+
|
|
347
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
|
|
348
|
+
|
|
349
|
+
afterEach(() => {
|
|
350
|
+
if (testSetup) {
|
|
351
|
+
testSetup.renderer.destroy()
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test("responds to keyboard", async () => {
|
|
356
|
+
testSetup = await createTestRenderer({
|
|
357
|
+
width: 40,
|
|
358
|
+
height: 10,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Create component that responds to keys
|
|
362
|
+
// ...
|
|
363
|
+
|
|
364
|
+
// Simulate keypress
|
|
365
|
+
testSetup.renderer.keyInput.emit("keypress", {
|
|
366
|
+
name: "enter",
|
|
367
|
+
sequence: "\r",
|
|
368
|
+
ctrl: false,
|
|
369
|
+
shift: false,
|
|
370
|
+
meta: false,
|
|
371
|
+
option: false,
|
|
372
|
+
eventType: "press",
|
|
373
|
+
repeated: false,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Render after the keypress
|
|
377
|
+
await testSetup.renderOnce()
|
|
378
|
+
|
|
379
|
+
expect(testSetup.captureCharFrame()).toContain("Selected")
|
|
380
|
+
})
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Testing Focus
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { test, expect, afterEach } from "bun:test"
|
|
387
|
+
import { createTestRenderer } from "@opentui/core/testing"
|
|
388
|
+
import { InputRenderable } from "@opentui/core"
|
|
389
|
+
|
|
390
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
|
|
391
|
+
|
|
392
|
+
afterEach(() => {
|
|
393
|
+
if (testSetup) {
|
|
394
|
+
testSetup.renderer.destroy()
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test("input receives focus", async () => {
|
|
399
|
+
testSetup = await createTestRenderer({
|
|
400
|
+
width: 40,
|
|
401
|
+
height: 10,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const input = new InputRenderable(testSetup.renderer, {
|
|
405
|
+
id: "test-input",
|
|
406
|
+
placeholder: "Type here",
|
|
407
|
+
})
|
|
408
|
+
testSetup.renderer.root.add(input)
|
|
409
|
+
|
|
410
|
+
input.focus()
|
|
411
|
+
|
|
412
|
+
expect(input.isFocused()).toBe(true)
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Test Organization
|
|
417
|
+
|
|
418
|
+
### File Structure
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
src/
|
|
422
|
+
├── components/
|
|
423
|
+
│ ├── Button.tsx
|
|
424
|
+
│ └── Button.test.tsx
|
|
425
|
+
├── hooks/
|
|
426
|
+
│ ├── useCounter.ts
|
|
427
|
+
│ └── useCounter.test.ts
|
|
428
|
+
└── test-utils.tsx
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Running Tests
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
# Run all tests
|
|
435
|
+
bun test
|
|
436
|
+
|
|
437
|
+
# Run specific test file
|
|
438
|
+
bun test src/components/Button.test.tsx
|
|
439
|
+
|
|
440
|
+
# Run with filter
|
|
441
|
+
bun test --filter "Button"
|
|
442
|
+
|
|
443
|
+
# Watch mode
|
|
444
|
+
bun test --watch
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Patterns
|
|
448
|
+
|
|
449
|
+
### Testing Conditional Rendering (React)
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
import { test, expect, afterEach } from "bun:test"
|
|
453
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
454
|
+
|
|
455
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
456
|
+
|
|
457
|
+
afterEach(() => {
|
|
458
|
+
if (testSetup) {
|
|
459
|
+
testSetup.renderer.destroy()
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test("shows loading state", async () => {
|
|
464
|
+
testSetup = await testRender(
|
|
465
|
+
<DataLoader loading={true} />,
|
|
466
|
+
{ width: 40, height: 10 }
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
await testSetup.renderOnce()
|
|
470
|
+
expect(testSetup.captureCharFrame()).toContain("Loading...")
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test("shows data when loaded", async () => {
|
|
474
|
+
testSetup = await testRender(
|
|
475
|
+
<DataLoader loading={false} data={["Item 1", "Item 2"]} />,
|
|
476
|
+
{ width: 40, height: 10 }
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
await testSetup.renderOnce()
|
|
480
|
+
const frame = testSetup.captureCharFrame()
|
|
481
|
+
expect(frame).toContain("Item 1")
|
|
482
|
+
expect(frame).toContain("Item 2")
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Testing Lists
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
test("renders all items", async () => {
|
|
490
|
+
const items = ["Apple", "Banana", "Cherry"]
|
|
491
|
+
|
|
492
|
+
testSetup = await testRender(
|
|
493
|
+
<ItemList items={items} />,
|
|
494
|
+
{ width: 40, height: 10 }
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
await testSetup.renderOnce()
|
|
498
|
+
const frame = testSetup.captureCharFrame()
|
|
499
|
+
|
|
500
|
+
items.forEach(item => {
|
|
501
|
+
expect(frame).toContain(item)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Testing Layouts
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
test("matches layout snapshot", async () => {
|
|
510
|
+
testSetup = await testRender(
|
|
511
|
+
<AppLayout />,
|
|
512
|
+
{ width: 120, height: 40 } // Larger viewport
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
await testSetup.renderOnce()
|
|
516
|
+
expect(testSetup.captureCharFrame()).toMatchSnapshot()
|
|
517
|
+
})
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Debugging Tests
|
|
521
|
+
|
|
522
|
+
### Print Frame Output
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
import { testRender } from "@opentui/react/test-utils"
|
|
526
|
+
|
|
527
|
+
test("debug output", async () => {
|
|
528
|
+
const testSetup = await testRender(
|
|
529
|
+
<MyComponent />,
|
|
530
|
+
{ width: 40, height: 10 }
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
await testSetup.renderOnce()
|
|
534
|
+
const frame = testSetup.captureCharFrame()
|
|
535
|
+
|
|
536
|
+
// Print to see what's rendered
|
|
537
|
+
console.log(frame)
|
|
538
|
+
|
|
539
|
+
expect(frame).toContain("expected")
|
|
540
|
+
})
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Verbose Mode
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
bun test --verbose
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Gotchas
|
|
550
|
+
|
|
551
|
+
### Async Rendering
|
|
552
|
+
|
|
553
|
+
Always call `renderOnce()` after setting up your component to ensure rendering is complete:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
const testSetup = await testRender(<MyComponent />, { width: 40, height: 10 })
|
|
557
|
+
await testSetup.renderOnce() // Required before capturing frame
|
|
558
|
+
const frame = testSetup.captureCharFrame()
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Test Isolation and Cleanup
|
|
562
|
+
|
|
563
|
+
Always destroy the renderer after each test to avoid resource leaks:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { afterEach } from "bun:test"
|
|
567
|
+
|
|
568
|
+
let testSetup: Awaited<ReturnType<typeof testRender>>
|
|
569
|
+
|
|
570
|
+
afterEach(() => {
|
|
571
|
+
if (testSetup) {
|
|
572
|
+
testSetup.renderer.destroy()
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test("test 1", async () => {
|
|
577
|
+
testSetup = await testRender(<Component1 />, { width: 40, height: 10 })
|
|
578
|
+
// ...
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
test("test 2", async () => {
|
|
582
|
+
testSetup = await testRender(<Component2 />, { width: 40, height: 10 })
|
|
583
|
+
// ...
|
|
584
|
+
})
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Snapshot Dimensions
|
|
588
|
+
|
|
589
|
+
Be consistent with test dimensions for stable snapshots:
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
const testSetup = await createTestRenderer({
|
|
593
|
+
width: 80, // Standard width
|
|
594
|
+
height: 24, // Standard height
|
|
595
|
+
})
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Running from Package Directory
|
|
599
|
+
|
|
600
|
+
Run tests from the package directory:
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
cd packages/core
|
|
604
|
+
bun test
|
|
605
|
+
|
|
606
|
+
# Not from repo root for package-specific tests
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## See Also
|
|
610
|
+
|
|
611
|
+
- [Core API](../core/api.md) - `createTestRenderer` and renderable classes
|
|
612
|
+
- [React Configuration](../react/configuration.md) - React test setup
|
|
613
|
+
- [Solid Configuration](../solid/configuration.md) - Solid test setup
|
|
614
|
+
- [Keyboard](../keyboard/REFERENCE.md) - Simulating key events in tests
|