@octanejs/testing-library 0.1.50 → 0.1.52
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 +15 -0
- package/package.json +7 -5
- package/src/pure.ts +46 -25
package/README.md
CHANGED
|
@@ -10,6 +10,9 @@ npm install @octanejs/testing-library
|
|
|
10
10
|
pnpm add @octanejs/testing-library
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
This package peers on **Octane `^0.2.5`**. Published 0.2.4 and earlier do not export
|
|
14
|
+
`isInActScope`, which both public entries import.
|
|
15
|
+
|
|
13
16
|
The split mirrors RTL's own architecture (and
|
|
14
17
|
`docs/react-library-compat-plan.md` §2): **`@testing-library/dom` is
|
|
15
18
|
framework-agnostic and reused verbatim** — every query, `screen`, `within`,
|
|
@@ -44,6 +47,11 @@ test('increments', () => {
|
|
|
44
47
|
- `screen`, `waitFor`, `within`, … — dom-testing-library, re-exported
|
|
45
48
|
- `fireEvent` — DOM event helpers, with focus/blur pairs for delegated handlers
|
|
46
49
|
|
|
50
|
+
Multiple `rerender` calls inside one `act` callback commit together when that
|
|
51
|
+
callback finishes. Await `act` before asserting results that depend on promises
|
|
52
|
+
or effects. Promise work settles even when timeout clocks are frozen; advance
|
|
53
|
+
fake timers explicitly when the behavior itself depends on a timer.
|
|
54
|
+
|
|
47
55
|
## Octane-specific surface
|
|
48
56
|
|
|
49
57
|
**Components are values in plain-`.ts` tests** — there's no JSX in a `.ts`
|
|
@@ -155,3 +163,10 @@ text commit on `tab()`, checkbox click ordering, `keyboard()`).
|
|
|
155
163
|
Current scope, known divergences, and verification status are tracked in the
|
|
156
164
|
generated [bindings status table](../../docs/bindings-status.md), sourced from
|
|
157
165
|
this package's [`status.json`](./status.json).
|
|
166
|
+
|
|
167
|
+
## Runtime requirement
|
|
168
|
+
|
|
169
|
+
This release requires Octane 0.2.5 or later in the 0.2 line. Upgrade Octane together
|
|
170
|
+
with testing-library so the `isInActScope` API used for nested `act` batching is
|
|
171
|
+
available. The packed-consumer checks exercise that API and rendering inside
|
|
172
|
+
an outer `act` scope.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/testing-library",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,14 +41,16 @@
|
|
|
41
41
|
"@testing-library/dom": "^10.4.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"octane": "^0.
|
|
44
|
+
"octane": "^0.2.5 || ^0.3.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@testing-library/user-event": "^14.6.1",
|
|
48
|
-
"
|
|
49
|
-
"react
|
|
48
|
+
"esbuild": "^0.28.1",
|
|
49
|
+
"react": "19.2.7",
|
|
50
|
+
"react-dom": "19.2.7",
|
|
51
|
+
"semver": "7.8.5",
|
|
50
52
|
"vitest": "^4.1.10",
|
|
51
|
-
"octane": "0.
|
|
53
|
+
"octane": "0.3.0"
|
|
52
54
|
},
|
|
53
55
|
"scripts": {
|
|
54
56
|
"test": "vitest run"
|
package/src/pure.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import {
|
|
24
24
|
act,
|
|
25
|
+
isInActScope,
|
|
25
26
|
createElement,
|
|
26
27
|
createRoot,
|
|
27
28
|
drainPassiveEffects,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
withSlot,
|
|
34
35
|
type ComponentBody,
|
|
35
36
|
type ElementDescriptor,
|
|
37
|
+
type OctaneNode,
|
|
36
38
|
type Root,
|
|
37
39
|
} from 'octane';
|
|
38
40
|
import {
|
|
@@ -108,6 +110,7 @@ configureDTL({
|
|
|
108
110
|
interface RootEntry {
|
|
109
111
|
container: Element;
|
|
110
112
|
root: Root;
|
|
113
|
+
valueRoot: boolean;
|
|
111
114
|
}
|
|
112
115
|
const mountedContainers = new Set<Element>();
|
|
113
116
|
const mountedRootEntries: RootEntry[] = [];
|
|
@@ -116,13 +119,11 @@ const mountedRootEntries: RootEntry[] = [];
|
|
|
116
119
|
// UI normalization — octane's two authoring forms into one mountable element.
|
|
117
120
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
121
|
|
|
119
|
-
/**
|
|
120
|
-
export type OctaneUI<P = any> = ComponentBody<P> |
|
|
122
|
+
/** A component body with optional props, or any Octane renderable. */
|
|
123
|
+
export type OctaneUI<P = any> = ComponentBody<P> | OctaneNode;
|
|
121
124
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// through component and rendered by the runtime's value-position renderer.
|
|
125
|
-
function ValueRoot(props: { children: unknown }): unknown {
|
|
125
|
+
// Arrays and host values share a stable value-rendering owner across rerenders.
|
|
126
|
+
function ValueRoot(props: { children: OctaneNode }): OctaneNode {
|
|
126
127
|
return props.children;
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -137,12 +138,12 @@ function toElement<P>(ui: OctaneUI<P>, props: P | undefined): ElementDescriptor
|
|
|
137
138
|
return ui;
|
|
138
139
|
}
|
|
139
140
|
if (typeof ui !== 'function') {
|
|
140
|
-
|
|
141
|
-
'render/rerender
|
|
142
|
-
|
|
143
|
-
);
|
|
141
|
+
if (props !== undefined) {
|
|
142
|
+
throw new Error('render/rerender only accepts the props option with a bare component.');
|
|
143
|
+
}
|
|
144
|
+
return createElement(ValueRoot, { children: ui });
|
|
144
145
|
}
|
|
145
|
-
return createElement(ui
|
|
146
|
+
return createElement(ui as ComponentBody<P>, props);
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function wrapUiIfNeeded(
|
|
@@ -152,8 +153,10 @@ function wrapUiIfNeeded(
|
|
|
152
153
|
return wrapperComponent ? createElement(wrapperComponent, { children: element }) : element;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
function mountable(element: ElementDescriptor): ElementDescriptor {
|
|
156
|
-
|
|
156
|
+
function mountable(element: ElementDescriptor, valueRoot: boolean): ElementDescriptor {
|
|
157
|
+
// Retain one value-rendering owner when rerender moves between an array,
|
|
158
|
+
// fragment, and single element. Keyed children keep their state across them.
|
|
159
|
+
return valueRoot && element.type !== ValueRoot
|
|
157
160
|
? createElement(ValueRoot, { children: element })
|
|
158
161
|
: element;
|
|
159
162
|
}
|
|
@@ -170,14 +173,21 @@ function mountable(element: ElementDescriptor): ElementDescriptor {
|
|
|
170
173
|
// `waitFor`/`findBy*`/`act` are for, same as RTL.
|
|
171
174
|
function settleSync(): void {
|
|
172
175
|
for (let i = 0; i < 50 && hasPendingWork(); i++) {
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
// Passive effects may schedule state updates too. They belong to this
|
|
177
|
+
// testing-library commit, so keep the drain inside its synchronous scope.
|
|
178
|
+
flushSync(() => {
|
|
179
|
+
drainPassiveEffects();
|
|
180
|
+
});
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
// Commit a render synchronously — flushSync for the render itself, then the
|
|
179
185
|
// cascade settle. Equivalent to RTL's `act(() => root.render(ui))` (sync form).
|
|
180
186
|
function commitRender(root: Root, element: ElementDescriptor): void {
|
|
187
|
+
if (isInActScope()) {
|
|
188
|
+
root.render(element);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
181
191
|
flushSync(() => {
|
|
182
192
|
root.render(element);
|
|
183
193
|
});
|
|
@@ -242,27 +252,32 @@ export function render<P = any>(ui: OctaneUI<P>, options: RenderOptions<P> = {})
|
|
|
242
252
|
container = baseElement.appendChild(document.createElement('div'));
|
|
243
253
|
}
|
|
244
254
|
|
|
245
|
-
const
|
|
255
|
+
const normalized = wrapUiIfNeeded(toElement(ui, props), WrapperComponent);
|
|
256
|
+
const previousEntry = mountedRootEntries.find((entry) => entry.container === container);
|
|
257
|
+
const valueRoot =
|
|
258
|
+
previousEntry?.valueRoot ??
|
|
259
|
+
(typeof normalized.type !== 'function' || normalized.type === ValueRoot);
|
|
260
|
+
const element = mountable(normalized, valueRoot);
|
|
246
261
|
|
|
247
262
|
let root: Root;
|
|
248
263
|
if (!mountedContainers.has(container)) {
|
|
249
264
|
if (hydrate) {
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
root = hydrateRoot(container, element);
|
|
265
|
+
// Adoption queues insertion/layout effects. Commit those before the
|
|
266
|
+
// passive settle, so descendants can use callbacks installed by them.
|
|
267
|
+
root = flushSync(() => hydrateRoot(container, element));
|
|
253
268
|
settleSync();
|
|
254
269
|
} else {
|
|
255
270
|
root = createRoot(container);
|
|
256
271
|
commitRender(root, element);
|
|
257
272
|
}
|
|
258
|
-
mountedRootEntries.push({ container, root });
|
|
273
|
+
mountedRootEntries.push({ container, root, valueRoot });
|
|
259
274
|
// Track the container regardless of whether WE created it, so cleanup()
|
|
260
275
|
// handles caller-supplied containers too (RTL does the same).
|
|
261
276
|
mountedContainers.add(container);
|
|
262
277
|
} else {
|
|
263
278
|
// Same container rendered again → reuse its root (same component updates
|
|
264
279
|
// props in place; a different component tears down and remounts).
|
|
265
|
-
root =
|
|
280
|
+
root = previousEntry!.root;
|
|
266
281
|
commitRender(root, element);
|
|
267
282
|
}
|
|
268
283
|
|
|
@@ -290,16 +305,22 @@ export function render<P = any>(ui: OctaneUI<P>, options: RenderOptions<P> = {})
|
|
|
290
305
|
let element: ElementDescriptor;
|
|
291
306
|
if (rerenderUi === undefined) {
|
|
292
307
|
element = toElement(ui, props); // re-run with the original props
|
|
293
|
-
} else if (
|
|
294
|
-
|
|
295
|
-
|
|
308
|
+
} else if (
|
|
309
|
+
rerenderUi !== null &&
|
|
310
|
+
typeof rerenderUi === 'object' &&
|
|
311
|
+
!isValidElement(rerenderUi) &&
|
|
312
|
+
!Array.isArray(rerenderUi) &&
|
|
313
|
+
Object.prototype.hasOwnProperty.call(rerenderUi, 'props')
|
|
314
|
+
) {
|
|
296
315
|
// `{ props }` shorthand: reuse the ORIGINAL component with new props.
|
|
297
316
|
const comp = isValidElement(ui) ? (ui as ElementDescriptor).type : ui;
|
|
298
317
|
element = createElement(comp as any, (rerenderUi as { props?: any }).props);
|
|
318
|
+
} else {
|
|
319
|
+
element = toElement(rerenderUi, rerenderOptions?.props);
|
|
299
320
|
}
|
|
300
321
|
// The wrapper is re-applied so component identity is stable across
|
|
301
322
|
// rerenders (same body ⇒ octane updates props in place).
|
|
302
|
-
commitRender(root, mountable(wrapUiIfNeeded(element, WrapperComponent)));
|
|
323
|
+
commitRender(root, mountable(wrapUiIfNeeded(element, WrapperComponent), valueRoot));
|
|
303
324
|
},
|
|
304
325
|
asFragment: () => document.createRange().createContextualFragment(container.innerHTML),
|
|
305
326
|
...(getQueriesForElement(baseElement, queries as any) as BoundFunctions<typeof defaultQueries>),
|