@octanejs/testing-library 0.1.0 → 0.1.2
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 +29 -9
- package/package.json +10 -2
- package/src/pure.ts +30 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @octanejs/testing-library
|
|
2
2
|
|
|
3
3
|
[React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
|
|
4
|
-
for the [octane](https://github.com/octanejs/octane)
|
|
4
|
+
for the [octane](https://github.com/octanejs/octane) UI framework.
|
|
5
5
|
|
|
6
6
|
The split mirrors RTL's own architecture (and
|
|
7
7
|
`docs/react-library-compat-plan.md` §2): **`@testing-library/dom` is
|
|
@@ -43,7 +43,8 @@ file, so `render` (and `rerender`) take two forms:
|
|
|
43
43
|
```ts
|
|
44
44
|
render(Counter, { props: { step: 2 }, wrapper: Providers }); // body + props option
|
|
45
45
|
render(createElement(Counter, { step: 2 }), { wrapper: Providers }); // RTL-style element
|
|
46
|
-
rerender(Counter, { step: 3 }); //
|
|
46
|
+
rerender(Counter, { props: { step: 3 } }); // symmetric with render's options
|
|
47
|
+
rerender({ props: { step: 3 } }); // shorthand: original component, new props
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
Same component ⇒ props update in place; a different component tears down and
|
|
@@ -61,9 +62,11 @@ React-specific remappings:
|
|
|
61
62
|
In octane `onChange` means the platform `change` event (fires on
|
|
62
63
|
commit/blur), and `onInput` fires per keystroke — port such tests to
|
|
63
64
|
`fireEvent.input` (or better, `@testing-library/user-event`, which emits
|
|
64
|
-
real event sequences).
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
real event sequences). Controlled components ARE supported (2026-07-08):
|
|
66
|
+
a `value`/`checked` prop drives the DOM property and reasserts on every
|
|
67
|
+
commit and after discrete events, exactly like React — only the synthetic
|
|
68
|
+
`onChange` normalization is absent, so a controlled text input updates its
|
|
69
|
+
state from `onInput`.
|
|
67
70
|
- **No enter/leave/focus double-dispatch.** RTL's `fireEvent.mouseEnter` also
|
|
68
71
|
fires `mouseover` (and `focus` fires `focusin`, `select` fires `keyup`, …)
|
|
69
72
|
purely to feed React's plugin system, which listens to different native
|
|
@@ -76,11 +79,12 @@ React-specific remappings:
|
|
|
76
79
|
drain via dom-testing-library's `eventWrapper`, so non-discrete/programmatic
|
|
77
80
|
events also commit — with their `useEffect` cascades — before `fireEvent`
|
|
78
81
|
returns (the equivalent of RTL's `act()` around each dispatch).
|
|
79
|
-
- **Host elements at the root
|
|
82
|
+
- **Host elements at the root are `container.firstChild`, like RTL.**
|
|
80
83
|
`render(createElement('div', …))` goes through octane's value-position
|
|
81
|
-
renderer,
|
|
82
|
-
|
|
83
|
-
(`render(App, …)`) mount their template
|
|
84
|
+
renderer, which mounts a lone host element anchorless (the element
|
|
85
|
+
self-delimits, no comment markers) — so RTL's `container.firstChild` idiom
|
|
86
|
+
works as-is. Component roots (`render(App, …)`) mount their template
|
|
87
|
+
directly, also without anchors.
|
|
84
88
|
- **`renderHook` and hook slots.** Octane hooks are keyed by compiler-assigned
|
|
85
89
|
call-site slots. Hook callbacks written in your test files Just Work — the
|
|
86
90
|
vite plugin's surgical pass slots base-hook calls in plain `.ts`, and
|
|
@@ -112,3 +116,19 @@ afterEach(cleanup);
|
|
|
112
116
|
|
|
113
117
|
`@octanejs/testing-library/pure` skips the side effects entirely, exactly like
|
|
114
118
|
`@testing-library/react/pure`.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## `@testing-library/user-event`
|
|
122
|
+
|
|
123
|
+
Works as-is — no octane adapter needed. `user-event` is framework-agnostic and
|
|
124
|
+
dispatches **real native events**, which is exactly octane's event model (a
|
|
125
|
+
better fit than React, where it relies on the synthetic layer picking natives
|
|
126
|
+
up). Install it alongside this package and use it unchanged; the pairing is
|
|
127
|
+
pinned by `tests/user-event.test.ts` (click, `type()` per-keystroke `onInput`,
|
|
128
|
+
`keyboard()`).
|
|
129
|
+
|
|
130
|
+
## Status
|
|
131
|
+
|
|
132
|
+
Current scope, known divergences, and verification status are tracked in the
|
|
133
|
+
generated [bindings status table](../../docs/bindings-status.md), sourced from
|
|
134
|
+
this package's [`status.json`](./status.json).
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/testing-library",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"octane": {
|
|
7
|
+
"hookSlots": {
|
|
8
|
+
"manual": [
|
|
9
|
+
"src"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
6
13
|
"description": "React Testing Library for the octane renderer — reuses the framework-agnostic @testing-library/dom verbatim and ports only react-testing-library's thin React layer (render/cleanup/renderHook) onto octane's createRoot + act.",
|
|
7
14
|
"author": {
|
|
8
15
|
"name": "Dominic Gannaway",
|
|
@@ -29,9 +36,10 @@
|
|
|
29
36
|
},
|
|
30
37
|
"dependencies": {
|
|
31
38
|
"@testing-library/dom": "^10.4.1",
|
|
32
|
-
"octane": "0.1.
|
|
39
|
+
"octane": "0.1.5"
|
|
33
40
|
},
|
|
34
41
|
"devDependencies": {
|
|
42
|
+
"@testing-library/user-event": "^14.6.1",
|
|
35
43
|
"vitest": "^4.1.9"
|
|
36
44
|
},
|
|
37
45
|
"scripts": {
|
package/src/pure.ts
CHANGED
|
@@ -221,8 +221,14 @@ export type RenderResult = {
|
|
|
221
221
|
container: HTMLElement;
|
|
222
222
|
baseElement: HTMLElement;
|
|
223
223
|
debug: DebugFn;
|
|
224
|
-
/**
|
|
225
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Re-render (same component ⇒ props update in place, like RTL's rerender).
|
|
226
|
+
* Three forms, symmetric with `render`:
|
|
227
|
+
* rerender(createElement(Comp, props)) — RTL-classic descriptor
|
|
228
|
+
* rerender(Comp, { props }) — component + options (same as render)
|
|
229
|
+
* rerender({ props }) — shorthand: original component, new props
|
|
230
|
+
*/
|
|
231
|
+
rerender: (ui?: OctaneUI | { props?: any }, options?: { props?: any }) => void;
|
|
226
232
|
unmount: () => void;
|
|
227
233
|
asFragment: () => DocumentFragment;
|
|
228
234
|
} & BoundFunctions<typeof defaultQueries>;
|
|
@@ -279,14 +285,25 @@ export function render<P = any>(ui: OctaneUI<P>, options: RenderOptions<P> = {})
|
|
|
279
285
|
flushSync(() => {
|
|
280
286
|
root.unmount();
|
|
281
287
|
});
|
|
288
|
+
// Deletion passive destroys are DEFERRED to the passive flush (React
|
|
289
|
+
// parity); RTL's unmount is act-wrapped, so it observes them flushed.
|
|
290
|
+
drainPassiveEffects();
|
|
282
291
|
},
|
|
283
|
-
rerender: (rerenderUi
|
|
292
|
+
rerender: (rerenderUi?: OctaneUI | { props?: any }, rerenderOptions?: { props?: any }) => {
|
|
293
|
+
// Normalize the three forms (see RenderResult.rerender) to one element.
|
|
294
|
+
let element: ElementDescriptor;
|
|
295
|
+
if (rerenderUi === undefined) {
|
|
296
|
+
element = toElement(ui, props); // re-run with the original props
|
|
297
|
+
} else if (isValidElement(rerenderUi) || typeof rerenderUi === 'function') {
|
|
298
|
+
element = toElement(rerenderUi as OctaneUI, rerenderOptions?.props);
|
|
299
|
+
} else {
|
|
300
|
+
// `{ props }` shorthand: reuse the ORIGINAL component with new props.
|
|
301
|
+
const comp = isValidElement(ui) ? (ui as ElementDescriptor).type : ui;
|
|
302
|
+
element = createElement(comp as any, (rerenderUi as { props?: any }).props);
|
|
303
|
+
}
|
|
284
304
|
// The wrapper is re-applied so component identity is stable across
|
|
285
305
|
// rerenders (same body ⇒ octane updates props in place).
|
|
286
|
-
commitRender(
|
|
287
|
-
root,
|
|
288
|
-
mountable(wrapUiIfNeeded(toElement(rerenderUi, rerenderProps), WrapperComponent)),
|
|
289
|
-
);
|
|
306
|
+
commitRender(root, mountable(wrapUiIfNeeded(element, WrapperComponent)));
|
|
290
307
|
},
|
|
291
308
|
asFragment: () => document.createRange().createContextualFragment(container.innerHTML),
|
|
292
309
|
...(getQueriesForElement(baseElement, queries as any) as BoundFunctions<typeof defaultQueries>),
|
|
@@ -302,6 +319,9 @@ export function cleanup(): void {
|
|
|
302
319
|
flushSync(() => {
|
|
303
320
|
root.unmount();
|
|
304
321
|
});
|
|
322
|
+
// See RenderResult.unmount — deletion passive destroys flush here too
|
|
323
|
+
// (RTL's auto-cleanup is act-wrapped).
|
|
324
|
+
drainPassiveEffects();
|
|
305
325
|
if (container.parentNode === document.body) {
|
|
306
326
|
document.body.removeChild(container);
|
|
307
327
|
}
|
|
@@ -378,7 +398,9 @@ export function renderHook<Result, Props = undefined>(
|
|
|
378
398
|
return {
|
|
379
399
|
result,
|
|
380
400
|
rerender: (rerenderCallbackProps?: Props) =>
|
|
381
|
-
baseRerender(TestComponent, {
|
|
401
|
+
baseRerender(TestComponent, {
|
|
402
|
+
props: { renderCallbackProps: rerenderCallbackProps as Props },
|
|
403
|
+
}),
|
|
382
404
|
unmount,
|
|
383
405
|
};
|
|
384
406
|
}
|