@octanejs/testing-library 0.1.0 → 0.1.1

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 (3) hide show
  1. package/README.md +29 -9
  2. package/package.json +10 -2
  3. 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) renderer.
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 }); // rerender takes bare props as the 2nd arg
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). There are **no controlled components**: `value` is a
65
- plain attribute and inputs are native/uncontrolled, so there's no React-style
66
- value re-assertion after events.
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 render between comment anchors.**
82
+ - **Host elements at the root are `container.firstChild`, like RTL.**
80
83
  `render(createElement('div', …))` goes through octane's value-position
81
- renderer, so `container.firstChild` is a comment node use
82
- `container.firstElementChild` (or just queries). Component roots
83
- (`render(App, …)`) mount their template directly, no anchors.
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.0",
3
+ "version": "0.1.1",
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.3"
39
+ "octane": "0.1.4"
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
- /** Re-render (same component ⇒ props update in place, like RTL's rerender). */
225
- rerender: (ui: OctaneUI, props?: any) => void;
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: OctaneUI, rerenderProps?: any) => {
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, { renderCallbackProps: rerenderCallbackProps as Props }),
401
+ baseRerender(TestComponent, {
402
+ props: { renderCallbackProps: rerenderCallbackProps as Props },
403
+ }),
382
404
  unmount,
383
405
  };
384
406
  }