@fictjs/testing-library 0.4.0 → 0.5.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/README.md +716 -0
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
# @fictjs/testing-library
|
|
2
|
+
|
|
3
|
+
Testing utilities for Fict components, built on top of `@testing-library/dom`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Core Concepts](#core-concepts)
|
|
11
|
+
- [API Reference](#api-reference)
|
|
12
|
+
- [Type Definitions](#type-definitions)
|
|
13
|
+
- [Testing Patterns](#testing-patterns)
|
|
14
|
+
- [Troubleshooting](#troubleshooting)
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
`@fictjs/testing-library` provides a set of utilities for testing Fict components in a manner similar to `@testing-library/react` and `@solidjs/testing-library`. It integrates seamlessly with Fict's reactive runtime while leveraging the familiar query APIs from `@testing-library/dom`.
|
|
19
|
+
|
|
20
|
+
### Key Features
|
|
21
|
+
|
|
22
|
+
| Feature | Description |
|
|
23
|
+
| ---------------------- | ---------------------------------------------------- |
|
|
24
|
+
| `render()` | Render Fict components and get query utilities |
|
|
25
|
+
| `cleanup()` | Automatic cleanup of rendered components |
|
|
26
|
+
| `renderHook()` | Test custom reactive hooks in isolation |
|
|
27
|
+
| `testEffect()` | Test effects with async assertions |
|
|
28
|
+
| `act()` | Flush pending microtasks and effects |
|
|
29
|
+
| Error Boundary Testing | Test error handling with `renderWithErrorBoundary()` |
|
|
30
|
+
| Suspense Testing | Test loading states with `renderWithSuspense()` |
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add -D @fictjs/testing-library
|
|
36
|
+
# or
|
|
37
|
+
npm install --save-dev @fictjs/testing-library
|
|
38
|
+
# or
|
|
39
|
+
yarn add -D @fictjs/testing-library
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Peer Dependencies:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm add @fictjs/runtime @testing-library/dom
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### Basic Component Testing
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { render, screen, cleanup } from '@fictjs/testing-library'
|
|
54
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
55
|
+
|
|
56
|
+
// Cleanup is automatic with Vitest/Jest
|
|
57
|
+
describe('Greeting', () => {
|
|
58
|
+
it('renders greeting message', () => {
|
|
59
|
+
const { getByText } = render(() => <Greeting name="World" />)
|
|
60
|
+
|
|
61
|
+
expect(getByText('Hello, World!')).toBeInTheDocument()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Testing with User Interactions
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { render, fireEvent } from '@fictjs/testing-library'
|
|
70
|
+
|
|
71
|
+
it('increments counter on click', async () => {
|
|
72
|
+
const { getByRole, getByText } = render(() => <Counter />)
|
|
73
|
+
|
|
74
|
+
const button = getByRole('button')
|
|
75
|
+
fireEvent.click(button)
|
|
76
|
+
|
|
77
|
+
expect(getByText('Count: 1')).toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Testing Reactive Hooks
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { renderHook } from '@fictjs/testing-library'
|
|
85
|
+
|
|
86
|
+
function useCounter(initial: number) {
|
|
87
|
+
let count = $state(initial)
|
|
88
|
+
const increment = () => count++
|
|
89
|
+
return { count: () => count, increment }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('increments counter', () => {
|
|
93
|
+
const { result } = renderHook(() => useCounter(0))
|
|
94
|
+
|
|
95
|
+
expect(result.current.count()).toBe(0)
|
|
96
|
+
result.current.increment()
|
|
97
|
+
expect(result.current.count()).toBe(1)
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Core Concepts
|
|
102
|
+
|
|
103
|
+
### 1. Component Rendering
|
|
104
|
+
|
|
105
|
+
The `render()` function renders a Fict component into a container and returns query utilities bound to that container:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const { container, getByText, queryByRole } = render(() => <MyComponent />)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The component is rendered inside a reactive root, enabling full reactivity during tests.
|
|
112
|
+
|
|
113
|
+
### 2. Automatic Cleanup
|
|
114
|
+
|
|
115
|
+
By default, cleanup runs automatically after each test when using Vitest or Jest. This can be disabled via the `FICT_TL_SKIP_AUTO_CLEANUP` environment variable.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// Cleanup runs automatically - no manual cleanup needed!
|
|
119
|
+
it('first test', () => {
|
|
120
|
+
render(() => <ComponentA />)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('second test', () => {
|
|
124
|
+
render(() => <ComponentB />) // Previous render is already cleaned up
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Testing Reactive Code
|
|
129
|
+
|
|
130
|
+
Use `renderHook()` to test reactive hooks and custom logic:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
const { result, rerender, cleanup } = renderHook(initial => useMyHook(initial), {
|
|
134
|
+
initialProps: [10],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Access hook return values
|
|
138
|
+
result.current.someMethod()
|
|
139
|
+
|
|
140
|
+
// Rerender with new props (note: state resets on rerender)
|
|
141
|
+
rerender([20])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> **Note:** `rerender()` disposes the previous root and creates a new one. Hook state does not persist across rerenders.
|
|
145
|
+
|
|
146
|
+
### 4. Testing Effects
|
|
147
|
+
|
|
148
|
+
Use `testEffect()` for testing async effects:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
const result = await testEffect<string>(done => {
|
|
152
|
+
const data = $state<string | null>(null)
|
|
153
|
+
|
|
154
|
+
$effect(() => {
|
|
155
|
+
if (data !== null) {
|
|
156
|
+
done(data) // Signal completion
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Simulate async operation
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
data = 'loaded'
|
|
163
|
+
}, 100)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(result).toBe('loaded')
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 5. Flushing Updates
|
|
170
|
+
|
|
171
|
+
Use `act()` to ensure all pending microtasks and effects are flushed:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
await act(() => {
|
|
175
|
+
result.current.increment()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// All effects have run, assertions are safe
|
|
179
|
+
expect(result.current.count()).toBe(1)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API Reference
|
|
183
|
+
|
|
184
|
+
### render
|
|
185
|
+
|
|
186
|
+
Render a Fict component for testing.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
function render<Q extends Queries = typeof queries>(
|
|
190
|
+
view: View,
|
|
191
|
+
options?: RenderOptions<Q>,
|
|
192
|
+
): RenderResult<Q>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
|
|
197
|
+
| Name | Type | Description |
|
|
198
|
+
| --------------------- | ---------------- | ----------------------------------------------------------------- |
|
|
199
|
+
| `view` | `() => FictNode` | A function that returns the component to render |
|
|
200
|
+
| `options.container` | `HTMLElement?` | Container element to render into |
|
|
201
|
+
| `options.baseElement` | `HTMLElement?` | Base element for queries (defaults to container or document.body) |
|
|
202
|
+
| `options.queries` | `Queries?` | Custom queries to use |
|
|
203
|
+
| `options.wrapper` | `Component?` | Wrapper component (e.g., for context providers) |
|
|
204
|
+
|
|
205
|
+
**Returns:**
|
|
206
|
+
|
|
207
|
+
| Property | Type | Description |
|
|
208
|
+
| ------------------------------- | ---------------------- | --------------------------------------- |
|
|
209
|
+
| `container` | `HTMLElement` | The container element |
|
|
210
|
+
| `baseElement` | `HTMLElement` | The base element for queries |
|
|
211
|
+
| `asFragment()` | `() => string` | Returns the container's innerHTML |
|
|
212
|
+
| `debug()` | `DebugFn` | Pretty-print the DOM to console |
|
|
213
|
+
| `unmount()` | `() => void` | Unmount and cleanup |
|
|
214
|
+
| `rerender(view)` | `(view: View) => void` | Re-render with a new view |
|
|
215
|
+
| `getByText`, `queryByRole`, ... | Query functions | All queries from `@testing-library/dom` |
|
|
216
|
+
|
|
217
|
+
**Example:**
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// Basic usage
|
|
221
|
+
const { getByText } = render(() => <MyComponent />)
|
|
222
|
+
expect(getByText('Hello')).toBeInTheDocument()
|
|
223
|
+
|
|
224
|
+
// With wrapper for context
|
|
225
|
+
const { getByText } = render(() => <MyComponent />, {
|
|
226
|
+
wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// With custom container
|
|
230
|
+
const container = document.createElement('div')
|
|
231
|
+
render(() => <MyComponent />, { container })
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### cleanup
|
|
237
|
+
|
|
238
|
+
Clean up all rendered components.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
function cleanup(): void
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Called automatically after each test. Can be called manually if needed.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### renderHook
|
|
249
|
+
|
|
250
|
+
Render a hook/reactive code for testing.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
function renderHook<Result, Props extends unknown[] = []>(
|
|
254
|
+
hookFn: (...args: Props) => Result,
|
|
255
|
+
options?: RenderHookOptions<Props> | Props,
|
|
256
|
+
): RenderHookResult<Result, Props>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Parameters:**
|
|
260
|
+
|
|
261
|
+
| Name | Type | Description |
|
|
262
|
+
| ---------------------- | ---------------------------- | --------------------------------- |
|
|
263
|
+
| `hookFn` | `(...args: Props) => Result` | The hook function to test |
|
|
264
|
+
| `options.initialProps` | `Props?` | Initial props to pass to the hook |
|
|
265
|
+
| `options.wrapper` | `Component?` | Wrapper component |
|
|
266
|
+
|
|
267
|
+
**Returns:**
|
|
268
|
+
|
|
269
|
+
| Property | Type | Description |
|
|
270
|
+
| ------------------ | ------------------------- | ----------------------------------------- |
|
|
271
|
+
| `result` | `{ current: Result }` | Container holding the hook's return value |
|
|
272
|
+
| `rerender(props?)` | `(props?: Props) => void` | Rerender with new props |
|
|
273
|
+
| `cleanup()` | `() => void` | Clean up the hook |
|
|
274
|
+
| `unmount()` | `() => void` | Alias for cleanup |
|
|
275
|
+
|
|
276
|
+
**Example:**
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
// Test a counter hook
|
|
280
|
+
const { result } = renderHook((initial: number) => useCounter(initial), {
|
|
281
|
+
initialProps: [10],
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
expect(result.current.count()).toBe(10)
|
|
285
|
+
result.current.increment()
|
|
286
|
+
expect(result.current.count()).toBe(11)
|
|
287
|
+
|
|
288
|
+
// Shorthand array syntax for initial props
|
|
289
|
+
const { result } = renderHook((a, b) => useMyHook(a, b), ['foo', 42])
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### testEffect
|
|
295
|
+
|
|
296
|
+
Test an effect asynchronously.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
function testEffect<T = void>(fn: TestEffectCallback<T>): Promise<T>
|
|
300
|
+
|
|
301
|
+
type TestEffectCallback<T> = (done: (result: T) => void) => void
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Example:**
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
// Test async data loading
|
|
308
|
+
const result = await testEffect<number>(done => {
|
|
309
|
+
const count = $state(0)
|
|
310
|
+
|
|
311
|
+
$effect(() => {
|
|
312
|
+
if (count === 3) {
|
|
313
|
+
done(count)
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
count = 1
|
|
318
|
+
count = 2
|
|
319
|
+
count = 3 // Triggers done()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
expect(result).toBe(3)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### act
|
|
328
|
+
|
|
329
|
+
Run updates and flush pending microtasks/effects.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
async function act<T>(fn: () => T | Promise<T>): Promise<T>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Example:**
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
await act(() => {
|
|
339
|
+
result.current.increment()
|
|
340
|
+
})
|
|
341
|
+
// All effects have been flushed
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### flush
|
|
347
|
+
|
|
348
|
+
Flush pending microtasks.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
function flush(): Promise<void>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### waitForCondition
|
|
357
|
+
|
|
358
|
+
Wait for a condition to be true.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
function waitForCondition(
|
|
362
|
+
condition: () => boolean,
|
|
363
|
+
options?: { timeout?: number; interval?: number },
|
|
364
|
+
): Promise<void>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Example:**
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
await waitForCondition(() => element.textContent === 'Loaded', {
|
|
371
|
+
timeout: 1000,
|
|
372
|
+
interval: 50,
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### renderWithErrorBoundary
|
|
379
|
+
|
|
380
|
+
Render a view wrapped in an ErrorBoundary.
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
function renderWithErrorBoundary<Q extends Queries>(
|
|
384
|
+
view: View,
|
|
385
|
+
options?: ErrorBoundaryRenderOptions<Q>,
|
|
386
|
+
): ErrorBoundaryRenderResult<Q>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Additional Options:**
|
|
390
|
+
|
|
391
|
+
| Name | Type | Description |
|
|
392
|
+
| ----------- | ---------------------------------------- | ----------------------------- |
|
|
393
|
+
| `fallback` | `FictNode \| ((err, reset) => FictNode)` | Fallback UI when error occurs |
|
|
394
|
+
| `onError` | `(err: unknown) => void` | Callback when error is caught |
|
|
395
|
+
| `resetKeys` | `unknown \| (() => unknown)` | Keys that trigger a reset |
|
|
396
|
+
|
|
397
|
+
**Additional Returns:**
|
|
398
|
+
|
|
399
|
+
| Property | Type | Description |
|
|
400
|
+
| ---------------------- | ------------------------ | -------------------------- |
|
|
401
|
+
| `triggerError(error)` | `(error: Error) => void` | Trigger an error |
|
|
402
|
+
| `resetErrorBoundary()` | `() => void` | Reset the error boundary |
|
|
403
|
+
| `isShowingFallback()` | `() => boolean` | Check if fallback is shown |
|
|
404
|
+
|
|
405
|
+
**Example:**
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
const onError = vi.fn()
|
|
409
|
+
const { isShowingFallback, triggerError } = renderWithErrorBoundary(
|
|
410
|
+
() => <ComponentThatMightThrow />,
|
|
411
|
+
{
|
|
412
|
+
fallback: err => <div data-testid="error">{err.message}</div>,
|
|
413
|
+
onError,
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
triggerError(new Error('Test error'))
|
|
418
|
+
expect(isShowingFallback()).toBe(true)
|
|
419
|
+
expect(onError).toHaveBeenCalled()
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### renderWithSuspense
|
|
425
|
+
|
|
426
|
+
Render a view wrapped in a Suspense boundary.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
function renderWithSuspense<Q extends Queries>(
|
|
430
|
+
view: View,
|
|
431
|
+
options?: SuspenseRenderOptions<Q>,
|
|
432
|
+
): SuspenseRenderResult<Q>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Additional Options:**
|
|
436
|
+
|
|
437
|
+
| Name | Type | Description |
|
|
438
|
+
| ----------- | ------------------------------ | ------------------------------- |
|
|
439
|
+
| `fallback` | `FictNode \| (() => FictNode)` | Loading UI while suspended |
|
|
440
|
+
| `onResolve` | `() => void` | Callback when suspense resolves |
|
|
441
|
+
| `onReject` | `(err: unknown) => void` | Callback when suspense rejects |
|
|
442
|
+
|
|
443
|
+
**Additional Returns:**
|
|
444
|
+
|
|
445
|
+
| Property | Type | Description |
|
|
446
|
+
| ----------------------------- | ----------------------------------------- | ---------------------------------- |
|
|
447
|
+
| `isShowingFallback()` | `() => boolean` | Check if loading fallback is shown |
|
|
448
|
+
| `waitForResolution(options?)` | `(options?: {timeout?}) => Promise<void>` | Wait for suspense to resolve |
|
|
449
|
+
|
|
450
|
+
**Example:**
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
const { token, resolve } = createTestSuspenseToken()
|
|
454
|
+
|
|
455
|
+
const { isShowingFallback, waitForResolution } = renderWithSuspense(
|
|
456
|
+
() => <AsyncComponent token={token} />,
|
|
457
|
+
{ fallback: <div>Loading...</div> },
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
expect(isShowingFallback()).toBe(true)
|
|
461
|
+
|
|
462
|
+
resolve()
|
|
463
|
+
await waitForResolution()
|
|
464
|
+
|
|
465
|
+
expect(isShowingFallback()).toBe(false)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
### createTestSuspenseToken
|
|
471
|
+
|
|
472
|
+
Create a test suspense token for controlling suspense in tests.
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
function createTestSuspenseToken(): TestSuspenseHandle
|
|
476
|
+
|
|
477
|
+
interface TestSuspenseHandle {
|
|
478
|
+
token: { then: PromiseLike<void>['then'] }
|
|
479
|
+
resolve: () => void
|
|
480
|
+
reject: (err: unknown) => void
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Example:**
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
const { token, resolve, reject } = createTestSuspenseToken()
|
|
488
|
+
|
|
489
|
+
// Throw token in component to trigger suspense
|
|
490
|
+
const Component = () => {
|
|
491
|
+
throw token
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Later, resolve to continue rendering
|
|
495
|
+
resolve()
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Type Definitions
|
|
499
|
+
|
|
500
|
+
### Core Types
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
/** A Fict view function */
|
|
504
|
+
type View = () => FictNode
|
|
505
|
+
|
|
506
|
+
/** Render options */
|
|
507
|
+
interface RenderOptions<Q extends Queries> {
|
|
508
|
+
container?: HTMLElement
|
|
509
|
+
baseElement?: HTMLElement
|
|
510
|
+
queries?: Q
|
|
511
|
+
wrapper?: Component<{ children: FictNode }>
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** RenderHook options */
|
|
515
|
+
interface RenderHookOptions<Props extends unknown[]> {
|
|
516
|
+
initialProps?: Props
|
|
517
|
+
wrapper?: Component<{ children: FictNode }>
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** TestEffect callback */
|
|
521
|
+
type TestEffectCallback<T> = (done: (result: T) => void) => void
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Result Types
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
/** Render result */
|
|
528
|
+
type RenderResult<Q extends Queries> = BoundFunctions<Q> & {
|
|
529
|
+
asFragment: () => string
|
|
530
|
+
container: HTMLElement
|
|
531
|
+
baseElement: HTMLElement
|
|
532
|
+
debug: DebugFn
|
|
533
|
+
unmount: () => void
|
|
534
|
+
rerender: (newView: View) => void
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** RenderHook result */
|
|
538
|
+
interface RenderHookResult<Result, Props extends unknown[]> {
|
|
539
|
+
result: { current: Result }
|
|
540
|
+
rerender: (newProps?: Props) => void
|
|
541
|
+
cleanup: () => void
|
|
542
|
+
unmount: () => void
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Testing Patterns
|
|
547
|
+
|
|
548
|
+
### Testing with Context Providers
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
const AllProviders = ({ children }: { children: FictNode }) => (
|
|
552
|
+
<ThemeProvider>
|
|
553
|
+
<AuthProvider>{children}</AuthProvider>
|
|
554
|
+
</ThemeProvider>
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const { getByText } = render(() => <MyComponent />, {
|
|
558
|
+
wrapper: AllProviders,
|
|
559
|
+
})
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Testing Reactive State Changes
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
import { renderHook, act } from '@fictjs/testing-library'
|
|
566
|
+
|
|
567
|
+
it('handles state updates', async () => {
|
|
568
|
+
const { result } = renderHook(() => {
|
|
569
|
+
const count = createSignal(0)
|
|
570
|
+
return { count, inc: () => count(count() + 1) }
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
await act(() => {
|
|
574
|
+
result.current.inc()
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
expect(result.current.count()).toBe(1)
|
|
578
|
+
})
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Testing Async Effects
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
import { testEffect } from '@fictjs/testing-library'
|
|
585
|
+
|
|
586
|
+
it('loads data asynchronously', async () => {
|
|
587
|
+
const result = await testEffect<Data>(done => {
|
|
588
|
+
const data = $state<Data | null>(null)
|
|
589
|
+
|
|
590
|
+
$effect(() => {
|
|
591
|
+
if (data) done(data)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
fetchData().then(d => {
|
|
595
|
+
data = d
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
expect(result).toEqual(expectedData)
|
|
600
|
+
})
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Testing Error Boundaries
|
|
604
|
+
|
|
605
|
+
```tsx
|
|
606
|
+
it('shows error fallback when child throws', () => {
|
|
607
|
+
const { isShowingFallback, triggerError, getByTestId } = renderWithErrorBoundary(
|
|
608
|
+
() => <ChildComponent />,
|
|
609
|
+
{
|
|
610
|
+
fallback: err => <div data-testid="error">{String(err)}</div>,
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
triggerError(new Error('Something went wrong'))
|
|
615
|
+
|
|
616
|
+
expect(isShowingFallback()).toBe(true)
|
|
617
|
+
expect(getByTestId('error').textContent).toContain('Something went wrong')
|
|
618
|
+
})
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Troubleshooting
|
|
622
|
+
|
|
623
|
+
### Common Issues
|
|
624
|
+
|
|
625
|
+
#### 1. "Cleanup not running between tests"
|
|
626
|
+
|
|
627
|
+
**Cause:** Auto-cleanup may not be set up correctly.
|
|
628
|
+
|
|
629
|
+
**Solution:** Ensure you're using Vitest or Jest, or manually call `cleanup()`:
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
import { cleanup } from '@fictjs/testing-library'
|
|
633
|
+
|
|
634
|
+
afterEach(() => {
|
|
635
|
+
cleanup()
|
|
636
|
+
})
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### 2. "Hook state resets on rerender"
|
|
640
|
+
|
|
641
|
+
**Cause:** This is expected behavior. `renderHook().rerender()` disposes the previous root and creates a new one.
|
|
642
|
+
|
|
643
|
+
**Solution:** If you need persistent state, test it within a single hook execution:
|
|
644
|
+
|
|
645
|
+
```tsx
|
|
646
|
+
const { result } = renderHook(() => useMyHook())
|
|
647
|
+
result.current.increment() // State persists here
|
|
648
|
+
result.current.increment() // And here
|
|
649
|
+
// rerender() creates a fresh hook instance
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
#### 3. "Effects not running in tests"
|
|
653
|
+
|
|
654
|
+
**Cause:** Effects are scheduled asynchronously.
|
|
655
|
+
|
|
656
|
+
**Solution:** Use `act()` or `await` with `flush()`:
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
await act(() => {
|
|
660
|
+
result.current.triggerEffect()
|
|
661
|
+
})
|
|
662
|
+
// Effects have now run
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
#### 4. "Queries not finding elements"
|
|
666
|
+
|
|
667
|
+
**Cause:** Element may not be in the queried container.
|
|
668
|
+
|
|
669
|
+
**Solution:** Use `debug()` to inspect the DOM:
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
const { debug, queryByText } = render(() => <MyComponent />)
|
|
673
|
+
debug() // Prints the DOM to console
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Configuration
|
|
677
|
+
|
|
678
|
+
#### Disabling Auto-Cleanup
|
|
679
|
+
|
|
680
|
+
Set the environment variable:
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
FICT_TL_SKIP_AUTO_CLEANUP=1 npm test
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
#### Using Custom Queries
|
|
687
|
+
|
|
688
|
+
```tsx
|
|
689
|
+
import { buildQueries } from '@testing-library/dom'
|
|
690
|
+
|
|
691
|
+
const queryAllByDataCy = (container, value) =>
|
|
692
|
+
container.querySelectorAll(`[data-cy="${value}"]`)
|
|
693
|
+
|
|
694
|
+
const [
|
|
695
|
+
queryByDataCy,
|
|
696
|
+
getAllByDataCy,
|
|
697
|
+
getByDataCy,
|
|
698
|
+
findAllByDataCy,
|
|
699
|
+
findByDataCy,
|
|
700
|
+
] = buildQueries(queryAllByDataCy, ...)
|
|
701
|
+
|
|
702
|
+
const { getByDataCy } = render(() => <MyComponent />, {
|
|
703
|
+
queries: { getByDataCy, queryByDataCy, ... }
|
|
704
|
+
})
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
## Related Packages
|
|
708
|
+
|
|
709
|
+
- `@fictjs/runtime` - Core reactive runtime
|
|
710
|
+
- `@fictjs/compiler` - JSX compiler
|
|
711
|
+
- `@fictjs/vite-plugin` - Vite integration
|
|
712
|
+
- `@testing-library/dom` - DOM testing utilities
|
|
713
|
+
|
|
714
|
+
## License
|
|
715
|
+
|
|
716
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fictjs/testing-library",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Testing utilities for Fict components, built on @testing-library/dom",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"jsdom": "^26.1.0",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
39
|
"vitest": "^4.0.18",
|
|
40
|
-
"@fictjs/compiler": "0.
|
|
41
|
-
"@fictjs/runtime": "0.
|
|
42
|
-
"@fictjs/vite-plugin": "0.
|
|
40
|
+
"@fictjs/compiler": "0.5.0",
|
|
41
|
+
"@fictjs/runtime": "0.5.0",
|
|
42
|
+
"@fictjs/vite-plugin": "0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"fict",
|