@rn-tools/core 3.0.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.
@@ -0,0 +1,483 @@
1
+ import * as React from "react";
2
+ import { render, waitFor } from "@testing-library/react/pure";
3
+ import { expect, it } from "vitest";
4
+ import {
5
+ RenderTreeNode,
6
+ RenderTree,
7
+ getRenderNodeActive,
8
+ getRenderNodeDepth,
9
+ useRenderTreeSelector,
10
+ } from "./render-tree";
11
+ import { RenderNodeProbe } from "../mocks/render-node-probe";
12
+
13
+ async function renderAndFlush(element: React.ReactElement) {
14
+ const renderer = render(element);
15
+ await waitFor(() => true);
16
+ return renderer;
17
+ }
18
+
19
+ async function updateAndFlush(
20
+ renderer: ReturnType<typeof render>,
21
+ element: React.ReactElement,
22
+ ) {
23
+ renderer.rerender(element);
24
+ await waitFor(() => true);
25
+ }
26
+
27
+ it("computes depth scoped by type", async () => {
28
+ const stackDepthRef = { current: null as unknown };
29
+ const nestedStackDepthRef = { current: null as unknown };
30
+ const screenDepthRef = { current: null as unknown };
31
+
32
+ await renderAndFlush(
33
+ <RenderTree>
34
+ <RenderTreeNode type="stack">
35
+ <RenderNodeProbe
36
+ render={(data) => {
37
+ stackDepthRef.current = data.depth;
38
+ return null;
39
+ }}
40
+ />
41
+ <RenderTreeNode type="stack">
42
+ <RenderNodeProbe
43
+ render={(data) => {
44
+ nestedStackDepthRef.current = data.depth;
45
+ return null;
46
+ }}
47
+ />
48
+ <RenderTreeNode type="screen">
49
+ <RenderNodeProbe
50
+ render={(data) => {
51
+ screenDepthRef.current = data.depth;
52
+ return null;
53
+ }}
54
+ />
55
+ </RenderTreeNode>
56
+ </RenderTreeNode>
57
+ </RenderTreeNode>
58
+ </RenderTree>,
59
+ );
60
+
61
+ expect(stackDepthRef.current).toBe(1);
62
+ expect(nestedStackDepthRef.current).toBe(2);
63
+ expect(screenDepthRef.current).toBe(1);
64
+ });
65
+
66
+ it("propagates active state through parents", async () => {
67
+ const screenActiveRef = { current: null as unknown };
68
+
69
+ const tree = await renderAndFlush(
70
+ <RenderTree>
71
+ <RenderTreeNode type="tabs" active>
72
+ <RenderTreeNode type="stack" active>
73
+ <RenderTreeNode type="screen">
74
+ <RenderNodeProbe
75
+ render={(data) => {
76
+ screenActiveRef.current = data.active;
77
+ return null;
78
+ }}
79
+ />
80
+ </RenderTreeNode>
81
+ </RenderTreeNode>
82
+ </RenderTreeNode>
83
+ </RenderTree>,
84
+ );
85
+
86
+ expect(screenActiveRef.current).toBe(true);
87
+
88
+ await updateAndFlush(
89
+ tree,
90
+ <RenderTree>
91
+ <RenderTreeNode type="tabs" active={false}>
92
+ <RenderTreeNode type="stack">
93
+ <RenderTreeNode type="screen">
94
+ <RenderNodeProbe
95
+ render={(data) => {
96
+ screenActiveRef.current = data.active;
97
+ return null;
98
+ }}
99
+ />
100
+ </RenderTreeNode>
101
+ </RenderTreeNode>
102
+ </RenderTreeNode>
103
+ </RenderTree>,
104
+ );
105
+
106
+ expect(screenActiveRef.current).toBe(false);
107
+ });
108
+
109
+ it("computes active in a type-agnostic way", async () => {
110
+ const leafActiveRef = { current: null as unknown };
111
+
112
+ await renderAndFlush(
113
+ <RenderTree>
114
+ <RenderTreeNode type="tabs" active={false}>
115
+ <RenderTreeNode type="stack" active>
116
+ <RenderTreeNode type="screen">
117
+ <RenderTreeNode type="panel" active>
118
+ <RenderNodeProbe
119
+ render={(data) => {
120
+ leafActiveRef.current = data.active;
121
+ return null;
122
+ }}
123
+ />
124
+ </RenderTreeNode>
125
+ </RenderTreeNode>
126
+ </RenderTreeNode>
127
+ </RenderTreeNode>
128
+ </RenderTree>,
129
+ );
130
+
131
+ expect(leafActiveRef.current).toBe(false);
132
+ });
133
+
134
+ it("tracks children by id", async () => {
135
+ const stackIdRef = { current: null as string | null };
136
+ const screenIdRef = { current: null as string | null };
137
+ const stackChildrenRef = { current: null as string[] | null };
138
+ const screenParentRef = { current: null as string | null };
139
+
140
+ await renderAndFlush(
141
+ <RenderTree>
142
+ <RenderTreeNode type="stack">
143
+ <RenderNodeProbe
144
+ render={(data) => {
145
+ stackIdRef.current = data.node.id;
146
+ stackChildrenRef.current = data.children.map((child) => child.id);
147
+ return null;
148
+ }}
149
+ />
150
+ <RenderTreeNode type="screen">
151
+ <RenderNodeProbe
152
+ render={(data) => {
153
+ screenIdRef.current = data.node.id;
154
+ screenParentRef.current = data.parent?.id ?? null;
155
+ return null;
156
+ }}
157
+ />
158
+ </RenderTreeNode>
159
+ </RenderTreeNode>
160
+ </RenderTree>,
161
+ );
162
+
163
+ expect(stackChildrenRef.current).toContain(screenIdRef.current);
164
+ expect(screenParentRef.current).toBe(stackIdRef.current);
165
+ });
166
+
167
+ it("updates depth when a stack is reparented", async () => {
168
+ const nestedStackDepthRef = { current: null as unknown };
169
+
170
+ const tree = await renderAndFlush(
171
+ <RenderTree>
172
+ <RenderTreeNode type="stack">
173
+ <RenderTreeNode type="stack">
174
+ <RenderNodeProbe
175
+ render={(data) => {
176
+ nestedStackDepthRef.current = data.depth;
177
+ return null;
178
+ }}
179
+ />
180
+ </RenderTreeNode>
181
+ </RenderTreeNode>
182
+ </RenderTree>,
183
+ );
184
+
185
+ expect(nestedStackDepthRef.current).toBe(2);
186
+
187
+ await updateAndFlush(
188
+ tree,
189
+ <RenderTree>
190
+ <RenderTreeNode type="stack">
191
+ <RenderNodeProbe
192
+ render={(data) => {
193
+ nestedStackDepthRef.current = data.depth;
194
+ return null;
195
+ }}
196
+ />
197
+ </RenderTreeNode>
198
+ </RenderTree>,
199
+ );
200
+
201
+ expect(nestedStackDepthRef.current).toBe(1);
202
+ });
203
+
204
+ it("updates parent/children relationships on unmount", async () => {
205
+ const stackIdRef = { current: null as string | null };
206
+ const stackChildrenRef = { current: null as string[] | null };
207
+ const screenParentIdRef = { current: null as string | null };
208
+
209
+ const tree = await renderAndFlush(
210
+ <RenderTree>
211
+ <RenderTreeNode type="stack">
212
+ <RenderNodeProbe
213
+ render={(data) => {
214
+ stackIdRef.current = data.node.id;
215
+ stackChildrenRef.current = data.children.map((child) => child.id);
216
+ return null;
217
+ }}
218
+ />
219
+ <RenderTreeNode type="screen" id="screen-a">
220
+ <RenderNodeProbe
221
+ render={(data) => {
222
+ screenParentIdRef.current = data.parent?.id ?? null;
223
+ return null;
224
+ }}
225
+ />
226
+ </RenderTreeNode>
227
+ </RenderTreeNode>
228
+ </RenderTree>,
229
+ );
230
+
231
+ expect(stackChildrenRef.current).toContain("screen-a");
232
+ expect(screenParentIdRef.current).toBe(stackIdRef.current);
233
+
234
+ await updateAndFlush(
235
+ tree,
236
+ <RenderTree>
237
+ <RenderTreeNode type="stack">
238
+ <RenderNodeProbe
239
+ render={(data) => {
240
+ stackChildrenRef.current = data.children.map((child) => child.id);
241
+ return null;
242
+ }}
243
+ />
244
+ </RenderTreeNode>
245
+ </RenderTree>,
246
+ );
247
+
248
+ expect(stackChildrenRef.current).not.toContain("screen-a");
249
+ });
250
+
251
+ it("respects local active overrides", async () => {
252
+ const screenActiveRef = { current: null as unknown };
253
+
254
+ const tree = await renderAndFlush(
255
+ <RenderTree>
256
+ <RenderTreeNode type="stack" active>
257
+ <RenderTreeNode type="screen" active={false}>
258
+ <RenderNodeProbe
259
+ render={(data) => {
260
+ screenActiveRef.current = data.active;
261
+ return null;
262
+ }}
263
+ />
264
+ </RenderTreeNode>
265
+ </RenderTreeNode>
266
+ </RenderTree>,
267
+ );
268
+
269
+ expect(screenActiveRef.current).toBe(false);
270
+
271
+ await updateAndFlush(
272
+ tree,
273
+ <RenderTree>
274
+ <RenderTreeNode type="stack" active={false}>
275
+ <RenderTreeNode type="screen" active>
276
+ <RenderNodeProbe
277
+ render={(data) => {
278
+ screenActiveRef.current = data.active;
279
+ return null;
280
+ }}
281
+ />
282
+ </RenderTreeNode>
283
+ </RenderTreeNode>
284
+ </RenderTree>,
285
+ );
286
+
287
+ expect(screenActiveRef.current).toBe(false);
288
+ });
289
+
290
+ it("only re-renders nodes affected by an upstream change", async () => {
291
+ const panelARenders = { current: 0 };
292
+ const stackARenders = { current: 0 };
293
+ const stackBRenders = { current: 0 };
294
+
295
+ function PanelAView() {
296
+ useRenderTreeSelector((chart, id) => getRenderNodeActive(chart, id));
297
+ panelARenders.current += 1;
298
+ return <StackA />;
299
+ }
300
+
301
+ function PanelA(props: { active?: boolean }) {
302
+ return (
303
+ <RenderTreeNode type="panel" active={props.active}>
304
+ <PanelAView />
305
+ </RenderTreeNode>
306
+ );
307
+ }
308
+
309
+ function StackAView() {
310
+ useRenderTreeSelector((chart, id) => getRenderNodeActive(chart, id));
311
+ stackARenders.current += 1;
312
+ return <>A</>;
313
+ }
314
+
315
+ function StackA() {
316
+ return (
317
+ <RenderTreeNode type="stack">
318
+ <StackAView />
319
+ </RenderTreeNode>
320
+ );
321
+ }
322
+
323
+ const StackB = React.memo(function StackB() {
324
+ return (
325
+ <RenderTreeNode type="stack">
326
+ <StackBView />
327
+ </RenderTreeNode>
328
+ );
329
+ });
330
+
331
+ function StackBView() {
332
+ useRenderTreeSelector((chart, id) => getRenderNodeActive(chart, id));
333
+ stackBRenders.current += 1;
334
+ return <>B</>;
335
+ }
336
+
337
+ const tree = await renderAndFlush(
338
+ <RenderTree>
339
+ <RenderTreeNode type="tabs" active>
340
+ <PanelA active />
341
+ <StackB />
342
+ </RenderTreeNode>
343
+ </RenderTree>,
344
+ );
345
+
346
+ const initialPanelARenders = panelARenders.current;
347
+ const initialStackARenders = stackARenders.current;
348
+ const initialStackBRenders = stackBRenders.current;
349
+
350
+ await updateAndFlush(
351
+ tree,
352
+ <RenderTree>
353
+ <RenderTreeNode type="tabs" active>
354
+ <PanelA active={false} />
355
+ <StackB />
356
+ </RenderTreeNode>
357
+ </RenderTree>,
358
+ );
359
+
360
+ expect(panelARenders.current).toBeGreaterThan(initialPanelARenders);
361
+ expect(stackARenders.current).toBeGreaterThan(initialStackARenders);
362
+ expect(stackBRenders.current).toBe(initialStackBRenders);
363
+ });
364
+
365
+ it("re-renders siblings when a shared ancestor changes", async () => {
366
+ const stackARenders = { current: 0 };
367
+ const stackBRenders = { current: 0 };
368
+
369
+ function StackAView() {
370
+ useRenderTreeSelector((chart, id) => getRenderNodeActive(chart, id));
371
+ stackARenders.current += 1;
372
+ return <>A</>;
373
+ }
374
+
375
+ function StackA() {
376
+ return (
377
+ <RenderTreeNode type="stack">
378
+ <StackAView />
379
+ </RenderTreeNode>
380
+ );
381
+ }
382
+
383
+ function StackBView() {
384
+ useRenderTreeSelector((chart, id) => getRenderNodeActive(chart, id));
385
+ stackBRenders.current += 1;
386
+ return <>B</>;
387
+ }
388
+
389
+ function StackB() {
390
+ return (
391
+ <RenderTreeNode type="stack">
392
+ <StackBView />
393
+ </RenderTreeNode>
394
+ );
395
+ }
396
+
397
+ const tree = await renderAndFlush(
398
+ <RenderTree>
399
+ <RenderTreeNode type="tabs" active>
400
+ <StackA />
401
+ <StackB />
402
+ </RenderTreeNode>
403
+ </RenderTree>,
404
+ );
405
+
406
+ const initialStackARenders = stackARenders.current;
407
+ const initialStackBRenders = stackBRenders.current;
408
+
409
+ await updateAndFlush(
410
+ tree,
411
+ <RenderTree>
412
+ <RenderTreeNode type="tabs" active={false}>
413
+ <StackA />
414
+ <StackB />
415
+ </RenderTreeNode>
416
+ </RenderTree>,
417
+ );
418
+
419
+ expect(stackARenders.current).toBeGreaterThan(initialStackARenders);
420
+ expect(stackBRenders.current).toBeGreaterThan(initialStackBRenders);
421
+ });
422
+
423
+ it("only re-renders the reparented subtree when depth changes", async () => {
424
+ const innerStackRenders = { current: 0 };
425
+ const siblingStackRenders = { current: 0 };
426
+
427
+ function InnerStackView() {
428
+ useRenderTreeSelector((chart, id) => getRenderNodeDepth(chart, id));
429
+ innerStackRenders.current += 1;
430
+ return <>Inner</>;
431
+ }
432
+
433
+ function InnerStack() {
434
+ return (
435
+ <RenderTreeNode type="stack">
436
+ <InnerStackView />
437
+ </RenderTreeNode>
438
+ );
439
+ }
440
+
441
+ function OuterStack() {
442
+ return (
443
+ <RenderTreeNode type="stack">
444
+ <InnerStack />
445
+ </RenderTreeNode>
446
+ );
447
+ }
448
+
449
+ const SiblingStack = React.memo(function SiblingStack() {
450
+ return (
451
+ <RenderTreeNode type="stack">
452
+ <SiblingStackView />
453
+ </RenderTreeNode>
454
+ );
455
+ });
456
+
457
+ function SiblingStackView() {
458
+ useRenderTreeSelector((chart, id) => getRenderNodeDepth(chart, id));
459
+ siblingStackRenders.current += 1;
460
+ return <>Sibling</>;
461
+ }
462
+
463
+ const tree = await renderAndFlush(
464
+ <RenderTree>
465
+ <OuterStack />
466
+ <SiblingStack />
467
+ </RenderTree>,
468
+ );
469
+
470
+ const initialInnerStackRenders = innerStackRenders.current;
471
+ const initialSiblingStackRenders = siblingStackRenders.current;
472
+
473
+ await updateAndFlush(
474
+ tree,
475
+ <RenderTree>
476
+ <InnerStack />
477
+ <SiblingStack />
478
+ </RenderTree>,
479
+ );
480
+
481
+ expect(innerStackRenders.current).toBeGreaterThan(initialInnerStackRenders);
482
+ expect(siblingStackRenders.current).toBe(initialSiblingStackRenders);
483
+ });