@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.
Files changed (55) hide show
  1. package/.agents/skills/opentui/SKILL.md +198 -0
  2. package/.agents/skills/opentui/references/animation/REFERENCE.md +431 -0
  3. package/.agents/skills/opentui/references/components/REFERENCE.md +143 -0
  4. package/.agents/skills/opentui/references/components/code-diff.md +496 -0
  5. package/.agents/skills/opentui/references/components/containers.md +412 -0
  6. package/.agents/skills/opentui/references/components/inputs.md +531 -0
  7. package/.agents/skills/opentui/references/components/text-display.md +384 -0
  8. package/.agents/skills/opentui/references/core/REFERENCE.md +145 -0
  9. package/.agents/skills/opentui/references/core/api.md +506 -0
  10. package/.agents/skills/opentui/references/core/configuration.md +166 -0
  11. package/.agents/skills/opentui/references/core/gotchas.md +393 -0
  12. package/.agents/skills/opentui/references/core/patterns.md +448 -0
  13. package/.agents/skills/opentui/references/keyboard/REFERENCE.md +511 -0
  14. package/.agents/skills/opentui/references/layout/REFERENCE.md +337 -0
  15. package/.agents/skills/opentui/references/layout/patterns.md +444 -0
  16. package/.agents/skills/opentui/references/react/REFERENCE.md +174 -0
  17. package/.agents/skills/opentui/references/react/api.md +435 -0
  18. package/.agents/skills/opentui/references/react/configuration.md +301 -0
  19. package/.agents/skills/opentui/references/react/gotchas.md +443 -0
  20. package/.agents/skills/opentui/references/react/patterns.md +501 -0
  21. package/.agents/skills/opentui/references/solid/REFERENCE.md +201 -0
  22. package/.agents/skills/opentui/references/solid/api.md +543 -0
  23. package/.agents/skills/opentui/references/solid/configuration.md +315 -0
  24. package/.agents/skills/opentui/references/solid/gotchas.md +415 -0
  25. package/.agents/skills/opentui/references/solid/patterns.md +558 -0
  26. package/.agents/skills/opentui/references/testing/REFERENCE.md +614 -0
  27. package/.claude/skills/opentui/SKILL.md +198 -0
  28. package/.claude/skills/opentui/references/animation/REFERENCE.md +431 -0
  29. package/.claude/skills/opentui/references/components/REFERENCE.md +143 -0
  30. package/.claude/skills/opentui/references/components/code-diff.md +496 -0
  31. package/.claude/skills/opentui/references/components/containers.md +412 -0
  32. package/.claude/skills/opentui/references/components/inputs.md +531 -0
  33. package/.claude/skills/opentui/references/components/text-display.md +384 -0
  34. package/.claude/skills/opentui/references/core/REFERENCE.md +145 -0
  35. package/.claude/skills/opentui/references/core/api.md +506 -0
  36. package/.claude/skills/opentui/references/core/configuration.md +166 -0
  37. package/.claude/skills/opentui/references/core/gotchas.md +393 -0
  38. package/.claude/skills/opentui/references/core/patterns.md +448 -0
  39. package/.claude/skills/opentui/references/keyboard/REFERENCE.md +511 -0
  40. package/.claude/skills/opentui/references/layout/REFERENCE.md +337 -0
  41. package/.claude/skills/opentui/references/layout/patterns.md +444 -0
  42. package/.claude/skills/opentui/references/react/REFERENCE.md +174 -0
  43. package/.claude/skills/opentui/references/react/api.md +435 -0
  44. package/.claude/skills/opentui/references/react/configuration.md +301 -0
  45. package/.claude/skills/opentui/references/react/gotchas.md +443 -0
  46. package/.claude/skills/opentui/references/react/patterns.md +501 -0
  47. package/.claude/skills/opentui/references/solid/REFERENCE.md +201 -0
  48. package/.claude/skills/opentui/references/solid/api.md +543 -0
  49. package/.claude/skills/opentui/references/solid/configuration.md +315 -0
  50. package/.claude/skills/opentui/references/solid/gotchas.md +415 -0
  51. package/.claude/skills/opentui/references/solid/patterns.md +558 -0
  52. package/.claude/skills/opentui/references/testing/REFERENCE.md +614 -0
  53. package/index.ts +169 -38
  54. package/package.json +1 -1
  55. 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