react-native-screen-transitions 3.2.0-beta.3 → 3.2.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 (138) hide show
  1. package/README.md +327 -672
  2. package/lib/commonjs/shared/components/screen-lifecycle.js +9 -133
  3. package/lib/commonjs/shared/components/screen-lifecycle.js.map +1 -1
  4. package/lib/commonjs/shared/constants.js +1 -0
  5. package/lib/commonjs/shared/constants.js.map +1 -1
  6. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +3 -0
  7. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  8. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +127 -0
  9. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -0
  10. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js +35 -0
  11. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js.map +1 -0
  12. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js +58 -0
  13. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js.map +1 -0
  14. package/lib/commonjs/shared/hooks/navigation/use-history.js +24 -0
  15. package/lib/commonjs/shared/hooks/navigation/use-history.js.map +1 -0
  16. package/lib/commonjs/shared/index.js +7 -0
  17. package/lib/commonjs/shared/index.js.map +1 -1
  18. package/lib/commonjs/shared/providers/screen/keys.provider.js +0 -4
  19. package/lib/commonjs/shared/providers/screen/keys.provider.js.map +1 -1
  20. package/lib/commonjs/shared/providers/screen/screen-composer.js +7 -5
  21. package/lib/commonjs/shared/providers/screen/screen-composer.js.map +1 -1
  22. package/lib/commonjs/shared/providers/screen/styles.provider.js +41 -32
  23. package/lib/commonjs/shared/providers/screen/styles.provider.js.map +1 -1
  24. package/lib/commonjs/shared/providers/stack/direct.provider.js +9 -0
  25. package/lib/commonjs/shared/providers/stack/direct.provider.js.map +1 -1
  26. package/lib/commonjs/shared/providers/stack/managed.provider.js +9 -0
  27. package/lib/commonjs/shared/providers/stack/managed.provider.js.map +1 -1
  28. package/lib/commonjs/shared/stores/animation.store.js +3 -13
  29. package/lib/commonjs/shared/stores/animation.store.js.map +1 -1
  30. package/lib/commonjs/shared/stores/history.store.js +185 -0
  31. package/lib/commonjs/shared/stores/history.store.js.map +1 -0
  32. package/lib/commonjs/shared/types/stack.types.js.map +1 -1
  33. package/lib/commonjs/shared/utils/animation/start-screen-transition.js +5 -1
  34. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +1 -1
  35. package/lib/commonjs/shared/utils/bounds/index.js +19 -4
  36. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  37. package/lib/module/shared/components/screen-lifecycle.js +9 -132
  38. package/lib/module/shared/components/screen-lifecycle.js.map +1 -1
  39. package/lib/module/shared/constants.js +1 -0
  40. package/lib/module/shared/constants.js.map +1 -1
  41. package/lib/module/shared/hooks/animation/use-screen-animation.js +3 -0
  42. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  43. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +122 -0
  44. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -0
  45. package/lib/module/shared/hooks/lifecycle/use-open-transition.js +32 -0
  46. package/lib/module/shared/hooks/lifecycle/use-open-transition.js.map +1 -0
  47. package/lib/module/shared/hooks/lifecycle/use-screen-events.js +54 -0
  48. package/lib/module/shared/hooks/lifecycle/use-screen-events.js.map +1 -0
  49. package/lib/module/shared/hooks/navigation/use-history.js +20 -0
  50. package/lib/module/shared/hooks/navigation/use-history.js.map +1 -0
  51. package/lib/module/shared/index.js +1 -0
  52. package/lib/module/shared/index.js.map +1 -1
  53. package/lib/module/shared/providers/screen/keys.provider.js +0 -4
  54. package/lib/module/shared/providers/screen/keys.provider.js.map +1 -1
  55. package/lib/module/shared/providers/screen/screen-composer.js +7 -5
  56. package/lib/module/shared/providers/screen/screen-composer.js.map +1 -1
  57. package/lib/module/shared/providers/screen/styles.provider.js +41 -32
  58. package/lib/module/shared/providers/screen/styles.provider.js.map +1 -1
  59. package/lib/module/shared/providers/stack/direct.provider.js +10 -1
  60. package/lib/module/shared/providers/stack/direct.provider.js.map +1 -1
  61. package/lib/module/shared/providers/stack/managed.provider.js +10 -1
  62. package/lib/module/shared/providers/stack/managed.provider.js.map +1 -1
  63. package/lib/module/shared/stores/animation.store.js +4 -14
  64. package/lib/module/shared/stores/animation.store.js.map +1 -1
  65. package/lib/module/shared/stores/history.store.js +181 -0
  66. package/lib/module/shared/stores/history.store.js.map +1 -0
  67. package/lib/module/shared/types/stack.types.js.map +1 -1
  68. package/lib/module/shared/utils/animation/start-screen-transition.js +5 -1
  69. package/lib/module/shared/utils/animation/start-screen-transition.js.map +1 -1
  70. package/lib/module/shared/utils/bounds/index.js +19 -4
  71. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  72. package/lib/typescript/blank-stack/types.d.ts +0 -3
  73. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  74. package/lib/typescript/component-stack/types.d.ts +0 -3
  75. package/lib/typescript/component-stack/types.d.ts.map +1 -1
  76. package/lib/typescript/shared/components/screen-lifecycle.d.ts +4 -1
  77. package/lib/typescript/shared/components/screen-lifecycle.d.ts.map +1 -1
  78. package/lib/typescript/shared/constants.d.ts.map +1 -1
  79. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  80. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts +13 -0
  81. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts.map +1 -0
  82. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts +11 -0
  83. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts.map +1 -0
  84. package/lib/typescript/shared/hooks/lifecycle/use-screen-events.d.ts +7 -0
  85. package/lib/typescript/shared/hooks/lifecycle/use-screen-events.d.ts.map +1 -0
  86. package/lib/typescript/shared/hooks/navigation/use-history.d.ts +37 -0
  87. package/lib/typescript/shared/hooks/navigation/use-history.d.ts.map +1 -0
  88. package/lib/typescript/shared/index.d.ts +3 -2
  89. package/lib/typescript/shared/index.d.ts.map +1 -1
  90. package/lib/typescript/shared/providers/screen/keys.provider.d.ts +0 -6
  91. package/lib/typescript/shared/providers/screen/keys.provider.d.ts.map +1 -1
  92. package/lib/typescript/shared/providers/screen/screen-composer.d.ts.map +1 -1
  93. package/lib/typescript/shared/providers/screen/styles.provider.d.ts.map +1 -1
  94. package/lib/typescript/shared/providers/stack/direct.provider.d.ts.map +1 -1
  95. package/lib/typescript/shared/providers/stack/managed.provider.d.ts.map +1 -1
  96. package/lib/typescript/shared/stores/animation.store.d.ts +3 -4
  97. package/lib/typescript/shared/stores/animation.store.d.ts.map +1 -1
  98. package/lib/typescript/shared/stores/history.store.d.ts +82 -0
  99. package/lib/typescript/shared/stores/history.store.d.ts.map +1 -0
  100. package/lib/typescript/shared/types/animation.types.d.ts +8 -0
  101. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  102. package/lib/typescript/shared/types/bounds.types.d.ts +1 -1
  103. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  104. package/lib/typescript/shared/types/stack.types.d.ts +1 -0
  105. package/lib/typescript/shared/types/stack.types.d.ts.map +1 -1
  106. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +1 -1
  107. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  108. package/package.json +28 -2
  109. package/src/blank-stack/types.ts +0 -8
  110. package/src/component-stack/types.ts +0 -9
  111. package/src/shared/__tests__/history.store.test.ts +550 -0
  112. package/src/shared/components/screen-lifecycle.tsx +13 -149
  113. package/src/shared/constants.ts +1 -0
  114. package/src/shared/hooks/animation/use-screen-animation.tsx +4 -0
  115. package/src/shared/hooks/lifecycle/use-close-transition.ts +147 -0
  116. package/src/shared/hooks/lifecycle/use-open-transition.ts +30 -0
  117. package/src/shared/hooks/lifecycle/use-screen-events.ts +62 -0
  118. package/src/shared/hooks/navigation/use-history.ts +63 -0
  119. package/src/shared/index.ts +1 -0
  120. package/src/shared/providers/screen/keys.provider.tsx +0 -16
  121. package/src/shared/providers/screen/screen-composer.tsx +6 -10
  122. package/src/shared/providers/screen/styles.provider.tsx +40 -34
  123. package/src/shared/providers/stack/direct.provider.tsx +11 -1
  124. package/src/shared/providers/stack/managed.provider.tsx +11 -1
  125. package/src/shared/stores/animation.store.ts +6 -20
  126. package/src/shared/stores/history.store.ts +201 -0
  127. package/src/shared/types/animation.types.ts +9 -0
  128. package/src/shared/types/bounds.types.ts +1 -0
  129. package/src/shared/types/stack.types.ts +1 -0
  130. package/src/shared/utils/animation/start-screen-transition.ts +4 -1
  131. package/src/shared/utils/bounds/index.ts +29 -3
  132. package/lib/commonjs/shared/utils/read-shared-value.js +0 -17
  133. package/lib/commonjs/shared/utils/read-shared-value.js.map +0 -1
  134. package/lib/module/shared/utils/read-shared-value.js +0 -14
  135. package/lib/module/shared/utils/read-shared-value.js.map +0 -1
  136. package/lib/typescript/shared/utils/read-shared-value.d.ts +0 -7
  137. package/lib/typescript/shared/utils/read-shared-value.d.ts.map +0 -1
  138. package/src/shared/utils/read-shared-value.ts +0 -15
@@ -0,0 +1,550 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { HistoryStore } from "../stores/history.store";
3
+ import type { BaseStackDescriptor } from "../types/stack.types";
4
+
5
+ // Helper to create mock descriptors
6
+ const createDescriptor = (
7
+ name: string,
8
+ navigatorKey = "nav-1",
9
+ ): BaseStackDescriptor =>
10
+ ({
11
+ route: { key: `${name}-instance`, name },
12
+ navigation: { getState: () => ({ key: navigatorKey }) },
13
+ options: {},
14
+ render: () => null,
15
+ }) as unknown as BaseStackDescriptor;
16
+
17
+ // Helper to generate expected history key
18
+ const historyKey = (name: string, navigatorKey = "nav-1") =>
19
+ `${navigatorKey}:${name}`;
20
+
21
+ // Reset history before each test
22
+ beforeEach(() => {
23
+ HistoryStore._reset();
24
+ });
25
+
26
+ // =============================================================================
27
+ // Unit Tests - focus (LRU behavior)
28
+ // =============================================================================
29
+
30
+ describe("HistoryStore.focus - LRU behavior", () => {
31
+ it("adds new screen to history", () => {
32
+ const descriptor = createDescriptor("screen-a");
33
+
34
+ HistoryStore.focus(descriptor, "nav-1");
35
+
36
+ expect(HistoryStore.has(historyKey("screen-a"))).toBe(true);
37
+ expect(HistoryStore.size()).toBe(1);
38
+ });
39
+
40
+ it("adds multiple screens in order", () => {
41
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
42
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
43
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
44
+
45
+ const entries = HistoryStore.toArray();
46
+ expect(entries.length).toBe(3);
47
+ expect(entries[0].descriptor.route.name).toBe("a"); // oldest
48
+ expect(entries[1].descriptor.route.name).toBe("b");
49
+ expect(entries[2].descriptor.route.name).toBe("c"); // most recent
50
+ });
51
+
52
+ it("moves existing screen to top (LRU reorder)", () => {
53
+ // Initial order: [A, B, C, D, E]
54
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
55
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
56
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
57
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
58
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
59
+
60
+ // Focus B again - should move to top
61
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
62
+
63
+ const entries = HistoryStore.toArray();
64
+ expect(entries.length).toBe(5); // No duplicates
65
+ expect(entries.map((e) => e.descriptor.route.name)).toEqual([
66
+ "a",
67
+ "c",
68
+ "d",
69
+ "e",
70
+ "b", // B moved to top
71
+ ]);
72
+ });
73
+
74
+ it("LRU reorder - complex scenario (browser history simulation)", () => {
75
+ // Simulate: Click b1, b2, b3, b4, b5
76
+ HistoryStore.focus(createDescriptor("b1"), "nav-1");
77
+ HistoryStore.focus(createDescriptor("b2"), "nav-1");
78
+ HistoryStore.focus(createDescriptor("b3"), "nav-1");
79
+ HistoryStore.focus(createDescriptor("b4"), "nav-1");
80
+ HistoryStore.focus(createDescriptor("b5"), "nav-1");
81
+
82
+ expect(HistoryStore.toArray().map((e) => e.descriptor.route.name)).toEqual([
83
+ "b1",
84
+ "b2",
85
+ "b3",
86
+ "b4",
87
+ "b5",
88
+ ]);
89
+
90
+ // Click b1 again - moves to top, removed from original position
91
+ HistoryStore.focus(createDescriptor("b1"), "nav-1");
92
+
93
+ expect(HistoryStore.toArray().map((e) => e.descriptor.route.name)).toEqual([
94
+ "b2", // b1 was here, now gone
95
+ "b3",
96
+ "b4",
97
+ "b5",
98
+ "b1", // b1 moved to top
99
+ ]);
100
+
101
+ // Click b2 - moves to top
102
+ HistoryStore.focus(createDescriptor("b2"), "nav-1");
103
+
104
+ expect(HistoryStore.toArray().map((e) => e.descriptor.route.name)).toEqual([
105
+ "b3", // b2 was here, now gone
106
+ "b4",
107
+ "b5",
108
+ "b1",
109
+ "b2", // b2 moved to top
110
+ ]);
111
+ });
112
+
113
+ it("updates descriptor when refocusing", () => {
114
+ const original = createDescriptor("a");
115
+ const updated = {
116
+ ...createDescriptor("a"),
117
+ options: { title: "Updated" },
118
+ } as unknown as BaseStackDescriptor;
119
+
120
+ HistoryStore.focus(original, "nav-1");
121
+ HistoryStore.focus(updated, "nav-1");
122
+
123
+ const entry = HistoryStore.get(historyKey("a"));
124
+ expect(entry?.descriptor.options).toEqual({ title: "Updated" });
125
+ });
126
+ });
127
+
128
+ // =============================================================================
129
+ // Unit Tests - Limit / Eviction
130
+ // =============================================================================
131
+
132
+ describe("HistoryStore - limit and eviction", () => {
133
+ it("evicts oldest when limit exceeded", () => {
134
+ // Add 101 entries (limit is 100)
135
+ for (let i = 0; i < 101; i++) {
136
+ HistoryStore.focus(createDescriptor(`screen-${i}`), "nav-1");
137
+ }
138
+
139
+ expect(HistoryStore.size()).toBe(100);
140
+ expect(HistoryStore.has(historyKey("screen-0"))).toBe(false); // Oldest evicted
141
+ expect(HistoryStore.has(historyKey("screen-1"))).toBe(true); // Second oldest kept
142
+ expect(HistoryStore.has(historyKey("screen-100"))).toBe(true); // Most recent kept
143
+ });
144
+
145
+ it("evicts multiple when way over limit", () => {
146
+ // Add 105 entries
147
+ for (let i = 0; i < 105; i++) {
148
+ HistoryStore.focus(createDescriptor(`screen-${i}`), "nav-1");
149
+ }
150
+
151
+ expect(HistoryStore.size()).toBe(100);
152
+ expect(HistoryStore.has(historyKey("screen-0"))).toBe(false);
153
+ expect(HistoryStore.has(historyKey("screen-4"))).toBe(false);
154
+ expect(HistoryStore.has(historyKey("screen-5"))).toBe(true);
155
+ });
156
+
157
+ it("refocusing does not cause extra eviction", () => {
158
+ // Add 99 entries
159
+ for (let i = 0; i < 99; i++) {
160
+ HistoryStore.focus(createDescriptor(`screen-${i}`), "nav-1");
161
+ }
162
+
163
+ // Refocus an existing entry - should not trigger eviction
164
+ HistoryStore.focus(createDescriptor("screen-50"), "nav-1");
165
+
166
+ expect(HistoryStore.size()).toBe(99);
167
+ expect(HistoryStore.has(historyKey("screen-0"))).toBe(true);
168
+ });
169
+ });
170
+
171
+ // =============================================================================
172
+ // Unit Tests - Query methods
173
+ // =============================================================================
174
+
175
+ describe("HistoryStore.getMostRecent", () => {
176
+ it("returns undefined when empty", () => {
177
+ expect(HistoryStore.getMostRecent()).toBeUndefined();
178
+ });
179
+
180
+ it("returns most recent entry", () => {
181
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
182
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
183
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
184
+
185
+ const recent = HistoryStore.getMostRecent();
186
+ expect(recent?.descriptor.route.name).toBe("c");
187
+ });
188
+
189
+ it("returns newly focused entry after LRU reorder", () => {
190
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
191
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
192
+ HistoryStore.focus(createDescriptor("a"), "nav-1"); // Refocus A
193
+
194
+ const recent = HistoryStore.getMostRecent();
195
+ expect(recent?.descriptor.route.name).toBe("a");
196
+ });
197
+ });
198
+
199
+ describe("HistoryStore.getRecent", () => {
200
+ it("returns empty array when empty", () => {
201
+ expect(HistoryStore.getRecent(5)).toEqual([]);
202
+ });
203
+
204
+ it("returns N most recent entries (most recent first)", () => {
205
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
206
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
207
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
208
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
209
+
210
+ const recent = HistoryStore.getRecent(2);
211
+ expect(recent.length).toBe(2);
212
+ expect(recent[0].descriptor.route.name).toBe("d"); // Most recent first
213
+ expect(recent[1].descriptor.route.name).toBe("c");
214
+ });
215
+
216
+ it("returns all if N > size", () => {
217
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
218
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
219
+
220
+ const recent = HistoryStore.getRecent(10);
221
+ expect(recent.length).toBe(2);
222
+ });
223
+ });
224
+
225
+ describe("HistoryStore.get / has", () => {
226
+ it("get returns undefined for unknown key", () => {
227
+ expect(HistoryStore.get("unknown")).toBeUndefined();
228
+ });
229
+
230
+ it("has returns false for unknown key", () => {
231
+ expect(HistoryStore.has("unknown")).toBe(false);
232
+ });
233
+
234
+ it("get returns entry for known key", () => {
235
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
236
+
237
+ const entry = HistoryStore.get(historyKey("a"));
238
+ expect(entry).toBeDefined();
239
+ expect(entry?.descriptor.route.name).toBe("a");
240
+ expect(entry?.navigatorKey).toBe("nav-1");
241
+ });
242
+
243
+ it("has returns true for known key", () => {
244
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
245
+ expect(HistoryStore.has(historyKey("a"))).toBe(true);
246
+ });
247
+ });
248
+
249
+ // =============================================================================
250
+ // Unit Tests - Navigator scoped queries
251
+ // =============================================================================
252
+
253
+ describe("HistoryStore.getByNavigator", () => {
254
+ it("returns empty array for unknown navigator", () => {
255
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
256
+
257
+ expect(HistoryStore.getByNavigator("nav-2")).toEqual([]);
258
+ });
259
+
260
+ it("returns entries for specific navigator only", () => {
261
+ HistoryStore.focus(createDescriptor("a", "nav-1"), "nav-1");
262
+ HistoryStore.focus(createDescriptor("b", "nav-2"), "nav-2");
263
+ HistoryStore.focus(createDescriptor("c", "nav-1"), "nav-1");
264
+ HistoryStore.focus(createDescriptor("d", "nav-2"), "nav-2");
265
+
266
+ const nav1 = HistoryStore.getByNavigator("nav-1");
267
+ expect(nav1.length).toBe(2);
268
+ expect(nav1.map((e) => e.descriptor.route.name)).toEqual(["c", "a"]); // Most recent first
269
+
270
+ const nav2 = HistoryStore.getByNavigator("nav-2");
271
+ expect(nav2.length).toBe(2);
272
+ expect(nav2.map((e) => e.descriptor.route.name)).toEqual(["d", "b"]);
273
+ });
274
+
275
+ it("returns in recency order (most recent first)", () => {
276
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
277
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
278
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
279
+
280
+ const entries = HistoryStore.getByNavigator("nav-1");
281
+ expect(entries[0].descriptor.route.name).toBe("c"); // Most recent
282
+ expect(entries[2].descriptor.route.name).toBe("a"); // Oldest
283
+ });
284
+ });
285
+
286
+ describe("HistoryStore.clearNavigator", () => {
287
+ it("removes all entries for specified navigator", () => {
288
+ HistoryStore.focus(createDescriptor("a", "nav-1"), "nav-1");
289
+ HistoryStore.focus(createDescriptor("b", "nav-2"), "nav-2");
290
+ HistoryStore.focus(createDescriptor("c", "nav-1"), "nav-1");
291
+
292
+ HistoryStore.clearNavigator("nav-1");
293
+
294
+ expect(HistoryStore.size()).toBe(1);
295
+ expect(HistoryStore.has(historyKey("a", "nav-1"))).toBe(false);
296
+ expect(HistoryStore.has(historyKey("c", "nav-1"))).toBe(false);
297
+ expect(HistoryStore.has(historyKey("b", "nav-2"))).toBe(true);
298
+ });
299
+
300
+ it("does nothing for unknown navigator", () => {
301
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
302
+
303
+ HistoryStore.clearNavigator("nav-unknown");
304
+
305
+ expect(HistoryStore.size()).toBe(1);
306
+ });
307
+ });
308
+
309
+ // =============================================================================
310
+ // Unit Tests - getPath (for multi-waypoint interpolation)
311
+ // =============================================================================
312
+
313
+ describe("HistoryStore.getPath", () => {
314
+ it("returns empty array if fromKey not found", () => {
315
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
316
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
317
+
318
+ expect(HistoryStore.getPath("unknown", historyKey("b"))).toEqual([]);
319
+ });
320
+
321
+ it("returns empty array if toKey not found", () => {
322
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
323
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
324
+
325
+ expect(HistoryStore.getPath(historyKey("a"), "unknown")).toEqual([]);
326
+ });
327
+
328
+ it("returns path from older to newer (forward direction)", () => {
329
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
330
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
331
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
332
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
333
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
334
+
335
+ const path = HistoryStore.getPath(historyKey("a"), historyKey("e"));
336
+ expect(path).toEqual([
337
+ historyKey("a"),
338
+ historyKey("b"),
339
+ historyKey("c"),
340
+ historyKey("d"),
341
+ historyKey("e"),
342
+ ]);
343
+ });
344
+
345
+ it("returns path from newer to older (backward direction)", () => {
346
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
347
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
348
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
349
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
350
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
351
+
352
+ const path = HistoryStore.getPath(historyKey("e"), historyKey("a"));
353
+ expect(path).toEqual([
354
+ historyKey("e"),
355
+ historyKey("d"),
356
+ historyKey("c"),
357
+ historyKey("b"),
358
+ historyKey("a"),
359
+ ]);
360
+ });
361
+
362
+ it("returns path for partial range", () => {
363
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
364
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
365
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
366
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
367
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
368
+
369
+ const path = HistoryStore.getPath(historyKey("b"), historyKey("d"));
370
+ expect(path).toEqual([historyKey("b"), historyKey("c"), historyKey("d")]);
371
+ });
372
+
373
+ it("returns single key if from === to", () => {
374
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
375
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
376
+
377
+ const path = HistoryStore.getPath(historyKey("b"), historyKey("b"));
378
+ expect(path).toEqual([historyKey("b")]);
379
+ });
380
+
381
+ it("respects LRU reordering in path", () => {
382
+ // Initial: [A, B, C, D, E]
383
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
384
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
385
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
386
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
387
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
388
+
389
+ // Refocus B - now: [A, C, D, E, B]
390
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
391
+
392
+ // Path from A to B should reflect new order
393
+ const path = HistoryStore.getPath(historyKey("a"), historyKey("b"));
394
+ expect(path).toEqual([
395
+ historyKey("a"),
396
+ historyKey("c"),
397
+ historyKey("d"),
398
+ historyKey("e"),
399
+ historyKey("b"),
400
+ ]);
401
+ });
402
+ });
403
+
404
+ // =============================================================================
405
+ // Scenario Tests - Navigation Flows
406
+ // =============================================================================
407
+
408
+ describe("Scenario: DismissAll interpolation", () => {
409
+ it("provides path for dismissAll from E to A", () => {
410
+ // Navigate: A → B → C → D → E
411
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
412
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
413
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
414
+ HistoryStore.focus(createDescriptor("d"), "nav-1");
415
+ HistoryStore.focus(createDescriptor("e"), "nav-1");
416
+
417
+ // DismissAll from E to A - need path for interpolation
418
+ const path = HistoryStore.getPath(historyKey("e"), historyKey("a"));
419
+ expect(path).toEqual([
420
+ historyKey("e"),
421
+ historyKey("d"),
422
+ historyKey("c"),
423
+ historyKey("b"),
424
+ historyKey("a"),
425
+ ]);
426
+ });
427
+ });
428
+
429
+ describe("Scenario: Forward navigation", () => {
430
+ it("dismissed screen remains in history for forward nav", () => {
431
+ // Navigate: A → B → C
432
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
433
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
434
+ HistoryStore.focus(createDescriptor("c"), "nav-1");
435
+
436
+ // Simulate going back to B (C dismissed but NOT removed from history)
437
+ HistoryStore.focus(createDescriptor("b"), "nav-1");
438
+
439
+ // C is still in history (before B since B was just refocused)
440
+ expect(HistoryStore.has(historyKey("c"))).toBe(true);
441
+
442
+ // Order is now [A, C, B] - C didn't move, B moved to top
443
+ const entries = HistoryStore.toArray();
444
+ expect(entries.map((e) => e.descriptor.route.name)).toEqual([
445
+ "a",
446
+ "c",
447
+ "b",
448
+ ]);
449
+
450
+ // For forward nav, we could go back to C
451
+ // (In practice, forward nav would look at what was most recent before B)
452
+ });
453
+ });
454
+
455
+ describe("Scenario: Cross-navigator history", () => {
456
+ it("maintains global order across navigators", () => {
457
+ // Tab 1: A → B
458
+ HistoryStore.focus(createDescriptor("a", "tab-1"), "tab-1");
459
+ HistoryStore.focus(createDescriptor("b", "tab-1"), "tab-1");
460
+
461
+ // Switch to Tab 2: C → D
462
+ HistoryStore.focus(createDescriptor("c", "tab-2"), "tab-2");
463
+ HistoryStore.focus(createDescriptor("d", "tab-2"), "tab-2");
464
+
465
+ // Switch back to Tab 1, focus A
466
+ HistoryStore.focus(createDescriptor("a", "tab-1"), "tab-1");
467
+
468
+ // Global order: [B, C, D, A]
469
+ const entries = HistoryStore.toArray();
470
+ expect(entries.map((e) => e.descriptor.route.name)).toEqual([
471
+ "b",
472
+ "c",
473
+ "d",
474
+ "a",
475
+ ]);
476
+
477
+ // Tab-specific order
478
+ const tab1 = HistoryStore.getByNavigator("tab-1");
479
+ expect(tab1.map((e) => e.descriptor.route.name)).toEqual(["a", "b"]);
480
+
481
+ const tab2 = HistoryStore.getByNavigator("tab-2");
482
+ expect(tab2.map((e) => e.descriptor.route.name)).toEqual(["d", "c"]);
483
+ });
484
+
485
+ it("getMostRecent returns globally most recent regardless of navigator", () => {
486
+ HistoryStore.focus(createDescriptor("a", "tab-1"), "tab-1");
487
+ HistoryStore.focus(createDescriptor("b", "tab-2"), "tab-2");
488
+ HistoryStore.focus(createDescriptor("c", "tab-1"), "tab-1");
489
+
490
+ expect(HistoryStore.getMostRecent()?.descriptor.route.name).toBe("c");
491
+ expect(HistoryStore.getMostRecent()?.navigatorKey).toBe("tab-1");
492
+ });
493
+ });
494
+
495
+ describe("Scenario: Nested navigator cleanup", () => {
496
+ it("modal navigator cleanup does not affect parent", () => {
497
+ // Parent stack
498
+ HistoryStore.focus(createDescriptor("home", "stack-main"), "stack-main");
499
+ HistoryStore.focus(
500
+ createDescriptor("details", "stack-main"),
501
+ "stack-main",
502
+ );
503
+
504
+ // Modal opens with its own navigator
505
+ HistoryStore.focus(
506
+ createDescriptor("modal-a", "stack-modal"),
507
+ "stack-modal",
508
+ );
509
+ HistoryStore.focus(
510
+ createDescriptor("modal-b", "stack-modal"),
511
+ "stack-modal",
512
+ );
513
+
514
+ expect(HistoryStore.size()).toBe(4);
515
+
516
+ // Modal closes - cleanup modal navigator
517
+ HistoryStore.clearNavigator("stack-modal");
518
+
519
+ expect(HistoryStore.size()).toBe(2);
520
+ expect(HistoryStore.has(historyKey("home", "stack-main"))).toBe(true);
521
+ expect(HistoryStore.has(historyKey("details", "stack-main"))).toBe(true);
522
+ expect(HistoryStore.has(historyKey("modal-a", "stack-modal"))).toBe(false);
523
+ expect(HistoryStore.has(historyKey("modal-b", "stack-modal"))).toBe(false);
524
+ });
525
+ });
526
+
527
+ // =============================================================================
528
+ // Unit Tests - Subscribe / Snapshot (for React integration)
529
+ // =============================================================================
530
+
531
+ describe("HistoryStore.subscribe / getSnapshot", () => {
532
+ it("getSnapshot returns current state", () => {
533
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
534
+
535
+ const snapshot = HistoryStore.getSnapshot();
536
+ expect(snapshot.size).toBe(1);
537
+ expect(snapshot.get(historyKey("a"))).toBeDefined();
538
+ });
539
+
540
+ it("snapshot is updated after focus", () => {
541
+ const snapshot1 = HistoryStore.getSnapshot();
542
+ expect(snapshot1.size).toBe(0);
543
+
544
+ HistoryStore.focus(createDescriptor("a"), "nav-1");
545
+
546
+ const snapshot2 = HistoryStore.getSnapshot();
547
+ expect(snapshot2.size).toBe(1);
548
+ expect(snapshot1).not.toBe(snapshot2); // New reference
549
+ });
550
+ });