@praxisjs/core 2.1.0 → 2.3.0

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 (126) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/component/internals.d.ts +12 -2
  3. package/dist/component/internals.d.ts.map +1 -1
  4. package/dist/component/internals.js +25 -7
  5. package/dist/component/internals.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -2
  9. package/dist/internal.js.map +1 -1
  10. package/dist/reactive/history.d.ts +2 -0
  11. package/dist/reactive/history.d.ts.map +1 -1
  12. package/dist/reactive/history.js +2 -1
  13. package/dist/reactive/history.js.map +1 -1
  14. package/dist/signal/batch.d.ts +1 -3
  15. package/dist/signal/batch.d.ts.map +1 -1
  16. package/dist/signal/batch.js +4 -16
  17. package/dist/signal/batch.js.map +1 -1
  18. package/dist/signal/computed.d.ts.map +1 -1
  19. package/dist/signal/computed.js +19 -61
  20. package/dist/signal/computed.js.map +1 -1
  21. package/dist/signal/effect.d.ts +6 -12
  22. package/dist/signal/effect.d.ts.map +1 -1
  23. package/dist/signal/effect.js +11 -74
  24. package/dist/signal/effect.js.map +1 -1
  25. package/dist/signal/graph.d.ts +111 -0
  26. package/dist/signal/graph.d.ts.map +1 -0
  27. package/dist/signal/graph.js +513 -0
  28. package/dist/signal/graph.js.map +1 -0
  29. package/dist/signal/index.d.ts +2 -1
  30. package/dist/signal/index.d.ts.map +1 -1
  31. package/dist/signal/index.js +1 -0
  32. package/dist/signal/index.js.map +1 -1
  33. package/dist/signal/peek.d.ts.map +1 -1
  34. package/dist/signal/peek.js +2 -9
  35. package/dist/signal/peek.js.map +1 -1
  36. package/dist/signal/persisted.d.ts +11 -1
  37. package/dist/signal/persisted.d.ts.map +1 -1
  38. package/dist/signal/persisted.js +39 -5
  39. package/dist/signal/persisted.js.map +1 -1
  40. package/dist/signal/selector.d.ts +5 -0
  41. package/dist/signal/selector.d.ts.map +1 -0
  42. package/dist/signal/selector.js +128 -0
  43. package/dist/signal/selector.js.map +1 -0
  44. package/dist/signal/signal.d.ts +0 -5
  45. package/dist/signal/signal.d.ts.map +1 -1
  46. package/dist/signal/signal.js +11 -81
  47. package/dist/signal/signal.js.map +1 -1
  48. package/package.json +13 -4
  49. package/src/component/internals.ts +35 -10
  50. package/src/internal.ts +27 -0
  51. package/src/reactive/history.ts +4 -1
  52. package/src/signal/batch.ts +5 -19
  53. package/src/signal/computed.ts +20 -63
  54. package/src/signal/effect.ts +12 -83
  55. package/src/signal/graph.ts +562 -0
  56. package/src/signal/index.ts +2 -1
  57. package/src/signal/peek.ts +2 -8
  58. package/src/signal/persisted.ts +54 -5
  59. package/src/signal/selector.ts +140 -0
  60. package/src/signal/signal.ts +11 -79
  61. package/dist/__tests__/batch.test.d.ts +0 -2
  62. package/dist/__tests__/batch.test.d.ts.map +0 -1
  63. package/dist/__tests__/batch.test.js +0 -73
  64. package/dist/__tests__/batch.test.js.map +0 -1
  65. package/dist/__tests__/component.test.d.ts +0 -2
  66. package/dist/__tests__/component.test.d.ts.map +0 -1
  67. package/dist/__tests__/component.test.js +0 -124
  68. package/dist/__tests__/component.test.js.map +0 -1
  69. package/dist/__tests__/computed.test.d.ts +0 -2
  70. package/dist/__tests__/computed.test.d.ts.map +0 -1
  71. package/dist/__tests__/computed.test.js +0 -252
  72. package/dist/__tests__/computed.test.js.map +0 -1
  73. package/dist/__tests__/effect.test.d.ts +0 -2
  74. package/dist/__tests__/effect.test.d.ts.map +0 -1
  75. package/dist/__tests__/effect.test.js +0 -189
  76. package/dist/__tests__/effect.test.js.map +0 -1
  77. package/dist/__tests__/peek.test.d.ts +0 -2
  78. package/dist/__tests__/peek.test.d.ts.map +0 -1
  79. package/dist/__tests__/peek.test.js +0 -26
  80. package/dist/__tests__/peek.test.js.map +0 -1
  81. package/dist/__tests__/persisted-signal.test.d.ts +0 -2
  82. package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
  83. package/dist/__tests__/persisted-signal.test.js +0 -156
  84. package/dist/__tests__/persisted-signal.test.js.map +0 -1
  85. package/dist/__tests__/reactive.test.d.ts +0 -2
  86. package/dist/__tests__/reactive.test.d.ts.map +0 -1
  87. package/dist/__tests__/reactive.test.js +0 -264
  88. package/dist/__tests__/reactive.test.js.map +0 -1
  89. package/dist/__tests__/resource-focus.test.d.ts +0 -2
  90. package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
  91. package/dist/__tests__/resource-focus.test.js +0 -50
  92. package/dist/__tests__/resource-focus.test.js.map +0 -1
  93. package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
  94. package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
  95. package/dist/__tests__/resource-server-pass.test.js +0 -78
  96. package/dist/__tests__/resource-server-pass.test.js.map +0 -1
  97. package/dist/__tests__/resource.test.d.ts +0 -2
  98. package/dist/__tests__/resource.test.d.ts.map +0 -1
  99. package/dist/__tests__/resource.test.js +0 -336
  100. package/dist/__tests__/resource.test.js.map +0 -1
  101. package/dist/__tests__/signal.test.d.ts +0 -2
  102. package/dist/__tests__/signal.test.d.ts.map +0 -1
  103. package/dist/__tests__/signal.test.js +0 -186
  104. package/dist/__tests__/signal.test.js.map +0 -1
  105. package/dist/__tests__/synced.test.d.ts +0 -2
  106. package/dist/__tests__/synced.test.d.ts.map +0 -1
  107. package/dist/__tests__/synced.test.js +0 -142
  108. package/dist/__tests__/synced.test.js.map +0 -1
  109. package/dist/__tests__/track.test.d.ts +0 -2
  110. package/dist/__tests__/track.test.d.ts.map +0 -1
  111. package/dist/__tests__/track.test.js +0 -54
  112. package/dist/__tests__/track.test.js.map +0 -1
  113. package/src/__tests__/batch.test.ts +0 -87
  114. package/src/__tests__/component.test.ts +0 -165
  115. package/src/__tests__/computed.test.ts +0 -290
  116. package/src/__tests__/effect.test.ts +0 -226
  117. package/src/__tests__/peek.test.ts +0 -31
  118. package/src/__tests__/persisted-signal.test.ts +0 -194
  119. package/src/__tests__/reactive.test.ts +0 -305
  120. package/src/__tests__/resource-focus.test.ts +0 -61
  121. package/src/__tests__/resource-server-pass.test.ts +0 -90
  122. package/src/__tests__/resource.test.ts +0 -421
  123. package/src/__tests__/signal.test.ts +0 -213
  124. package/src/__tests__/synced.test.ts +0 -173
  125. package/src/__tests__/track.test.ts +0 -61
  126. package/tsconfig.json +0 -8
@@ -1,194 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, beforeEach, vi } from "vitest";
3
-
4
- import { persistedSignal } from "../signal/persisted";
5
-
6
- beforeEach(() => {
7
- localStorage.clear();
8
- });
9
-
10
- describe("persistedSignal", () => {
11
- it("returns initialValue when nothing is stored", () => {
12
- const s = persistedSignal("key1", 42);
13
- expect(s()).toBe(42);
14
- });
15
-
16
- it("reads existing value from localStorage", () => {
17
- localStorage.setItem("key2", "99");
18
- const s = persistedSignal("key2", 0);
19
- expect(s()).toBe(99);
20
- });
21
-
22
- it("persists set() to localStorage", () => {
23
- const s = persistedSignal("key3", 0);
24
- s.set(7);
25
- expect(localStorage.getItem("key3")).toBe("7");
26
- expect(s()).toBe(7);
27
- });
28
-
29
- it("persists update() to localStorage", () => {
30
- const s = persistedSignal("key4", 10);
31
- s.update((v) => v + 5);
32
- expect(localStorage.getItem("key4")).toBe("15");
33
- expect(s()).toBe(15);
34
- });
35
-
36
- it("removes the key when set to null", () => {
37
- const s = persistedSignal<string | null>("key5", "hello");
38
- s.set(null);
39
- expect(localStorage.getItem("key5")).toBeNull();
40
- });
41
-
42
- it("marks __isSignal = true", () => {
43
- const s = persistedSignal("key6", 0);
44
- expect(s.__isSignal).toBe(true);
45
- });
46
-
47
- it("supports custom serialize/deserialize", () => {
48
- const s = persistedSignal("key7", { x: 1 }, {
49
- serialize: (v) => JSON.stringify(v),
50
- deserialize: (raw) => JSON.parse(raw) as { x: number },
51
- });
52
- s.set({ x: 99 });
53
- expect(localStorage.getItem("key7")).toBe('{"x":99}');
54
- const s2 = persistedSignal("key7", { x: 0 }, {
55
- serialize: JSON.stringify,
56
- deserialize: (raw) => JSON.parse(raw) as { x: number },
57
- });
58
- expect(s2().x).toBe(99);
59
- });
60
-
61
- it("falls back to initialValue when deserialization fails", () => {
62
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
63
- localStorage.setItem("key8", "{ bad json");
64
- const s = persistedSignal("key8", 42);
65
- expect(s()).toBe(42);
66
- warn.mockRestore();
67
- });
68
-
69
- it("syncs from storage event on other tab", () => {
70
- const s = persistedSignal("key9", 1);
71
- window.dispatchEvent(
72
- new StorageEvent("storage", {
73
- key: "key9",
74
- newValue: "55",
75
- storageArea: localStorage,
76
- }),
77
- );
78
- expect(s()).toBe(55);
79
- });
80
-
81
- it("syncTabs=false does not add storage listener", () => {
82
- const s = persistedSignal("key10", 1, { syncTabs: false });
83
- s.set(5);
84
- // Just verify it works without errors
85
- expect(s()).toBe(5);
86
- });
87
-
88
- it("warns and falls back to initialValue when serialize throws on set", () => {
89
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
90
- const s = persistedSignal("key11", { x: 1 }, {
91
- serialize: () => { throw new Error("serialize fail"); },
92
- deserialize: JSON.parse as (v: string) => { x: number },
93
- });
94
- expect(() => s.set({ x: 2 })).not.toThrow();
95
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("Failed to serialize"), expect.any(Error));
96
- warn.mockRestore();
97
- });
98
-
99
- it("syncs to initialValue when storage event has newValue=null (key removed)", () => {
100
- const s = persistedSignal("key12", 42);
101
- s.set(99);
102
-
103
- window.dispatchEvent(
104
- new StorageEvent("storage", {
105
- key: "key12",
106
- newValue: null, // key removed in another tab
107
- storageArea: localStorage,
108
- }),
109
- );
110
- expect(s()).toBe(42); // falls back to initialValue
111
- });
112
-
113
- it("warns and falls back to initialValue when storage event deserialization fails", () => {
114
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
115
- const s = persistedSignal("key13", 0, {
116
- deserialize: () => { throw new Error("bad"); },
117
- });
118
-
119
- window.dispatchEvent(
120
- new StorageEvent("storage", {
121
- key: "key13",
122
- newValue: "invalid",
123
- storageArea: localStorage,
124
- }),
125
- );
126
- expect(s()).toBe(0);
127
- expect(warn).toHaveBeenCalledWith(
128
- expect.stringContaining("Failed to deserialize"),
129
- expect.any(Error),
130
- );
131
- warn.mockRestore();
132
- });
133
-
134
- it("ignores storage events from other keys", () => {
135
- const s = persistedSignal("key14", 1);
136
- window.dispatchEvent(
137
- new StorageEvent("storage", {
138
- key: "other-key",
139
- newValue: "999",
140
- storageArea: localStorage,
141
- }),
142
- );
143
- expect(s()).toBe(1);
144
- });
145
-
146
- it("set(undefined) removes the key from localStorage", () => {
147
- const s = persistedSignal<number | undefined>("key15", 5);
148
- s.set(undefined);
149
- expect(localStorage.getItem("key15")).toBeNull();
150
- });
151
-
152
- it("localStorage.setItem throws QuotaExceededError — signal state is updated but no crash", () => {
153
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
154
- const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
155
- const err = new DOMException("QuotaExceededError", "QuotaExceededError");
156
- throw err;
157
- });
158
-
159
- const s = persistedSignal("key16", 0);
160
- expect(() => s.set(42)).not.toThrow();
161
- // The in-memory signal value is still updated despite storage failure
162
- expect(s()).toBe(42);
163
-
164
- setItemSpy.mockRestore();
165
- warn.mockRestore();
166
- });
167
-
168
- it("syncTabs=true — storage event with empty string newValue is handled", () => {
169
- const s = persistedSignal("key17", 99);
170
- window.dispatchEvent(
171
- new StorageEvent("storage", {
172
- key: "key17",
173
- newValue: "",
174
- storageArea: localStorage,
175
- }),
176
- );
177
- // Empty string is falsy, so falls back to initialValue
178
- expect(s()).toBe(99);
179
- });
180
-
181
- it("syncTabs=true — storage event from sessionStorage is ignored", () => {
182
- const s = persistedSignal("key18", 10);
183
- s.set(20);
184
- window.dispatchEvent(
185
- new StorageEvent("storage", {
186
- key: "key18",
187
- newValue: "999",
188
- storageArea: sessionStorage,
189
- }),
190
- );
191
- // Should stay at 20 since the event is from sessionStorage, not localStorage
192
- expect(s()).toBe(20);
193
- });
194
- });
@@ -1,305 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import { history } from "../reactive/history";
4
- import { when, until, debounced } from "../reactive/reactive";
5
- import { computed } from "../signal/computed";
6
- import { signal } from "../signal/signal";
7
-
8
- // ---------- when ----------
9
-
10
- describe("when", () => {
11
- it("calls fn immediately when source is already truthy", () => {
12
- const s = signal<number>(5);
13
- const fn = vi.fn();
14
- when(s, fn);
15
- expect(fn).toHaveBeenCalledWith(5);
16
- });
17
-
18
- it("calls fn once the source becomes truthy", () => {
19
- const s = signal<number>(0);
20
- const fn = vi.fn();
21
- when(s, fn);
22
- expect(fn).not.toHaveBeenCalled();
23
- s.set(1);
24
- expect(fn).toHaveBeenCalledWith(1);
25
- });
26
-
27
- it("only fires once, even if source changes again", () => {
28
- const s = signal(0);
29
- const fn = vi.fn();
30
- when(s, fn);
31
- s.set(1);
32
- s.set(2);
33
- expect(fn).toHaveBeenCalledTimes(1);
34
- });
35
-
36
- it("does not re-fire after immediate truthy source changes again", () => {
37
- // Regression: when source is truthy on first run, stop?.() was a no-op
38
- // because `stop` hadn't been assigned yet. The effect must be cancelled
39
- // after firing so further source changes do not re-trigger fn.
40
- const s = signal(1); // immediately truthy
41
- const fn = vi.fn();
42
- when(s, fn);
43
- expect(fn).toHaveBeenCalledTimes(1);
44
- s.set(2);
45
- s.set(3);
46
- expect(fn).toHaveBeenCalledTimes(1); // must still be 1
47
- });
48
-
49
- it("does not call fn when source stays falsy", () => {
50
- const s = signal<number>(0);
51
- const fn = vi.fn();
52
- when(s, fn);
53
- s.set(0);
54
- expect(fn).not.toHaveBeenCalled();
55
- });
56
-
57
- it("with source that fires immediately on subscription does not crash", () => {
58
- const s = signal(1); // immediately truthy
59
- const fn = vi.fn();
60
- expect(() => when(s, fn)).not.toThrow();
61
- expect(fn).toHaveBeenCalledWith(1);
62
- });
63
- });
64
-
65
- // ---------- until ----------
66
-
67
- describe("until", () => {
68
- it("resolves immediately when source is already truthy", async () => {
69
- const s = signal(42);
70
- const value = await until(s);
71
- expect(value).toBe(42);
72
- });
73
-
74
- it("resolves when source becomes truthy", async () => {
75
- const s = signal<number>(0);
76
- const promise = until(s);
77
- s.set(7);
78
- expect(await promise).toBe(7);
79
- });
80
-
81
- it("the promise remains pending if source is always falsy (no-timeout behavior)", async () => {
82
- const s = signal<number>(0);
83
- let resolved = false;
84
- until(s).then(() => { resolved = true; });
85
- // Never set a truthy value
86
- await new Promise((res) => setTimeout(res, 10));
87
- expect(resolved).toBe(false);
88
- });
89
- });
90
-
91
- // ---------- debounced ----------
92
-
93
- describe("debounced", () => {
94
- it("returns initial value immediately", () => {
95
- vi.useFakeTimers();
96
- const s = signal(1);
97
- const d = debounced(s, 100);
98
- expect(d()).toBe(1);
99
- vi.useRealTimers();
100
- });
101
-
102
- it("debounces rapid updates", () => {
103
- vi.useFakeTimers();
104
- const s = signal(1);
105
- const d = debounced(s, 100);
106
-
107
- s.set(2);
108
- s.set(3);
109
- s.set(4);
110
-
111
- expect(d()).toBe(1); // not yet updated
112
-
113
- vi.advanceTimersByTime(100);
114
- expect(d()).toBe(4); // settled on last value
115
- vi.useRealTimers();
116
- });
117
-
118
- it("emits each value when updates are spaced apart", () => {
119
- vi.useFakeTimers();
120
- const s = signal(0);
121
- const d = debounced(s, 50);
122
-
123
- s.set(1);
124
- vi.advanceTimersByTime(50);
125
- expect(d()).toBe(1);
126
-
127
- s.set(2);
128
- vi.advanceTimersByTime(50);
129
- expect(d()).toBe(2);
130
- vi.useRealTimers();
131
- });
132
-
133
- it("delay=0 fires synchronously after setTimeout(0)", () => {
134
- vi.useFakeTimers();
135
- const s = signal("a");
136
- const d = debounced(s, 0);
137
-
138
- s.set("b");
139
- expect(d()).toBe("a"); // not yet
140
- vi.advanceTimersByTime(0);
141
- expect(d()).toBe("b");
142
- vi.useRealTimers();
143
- });
144
-
145
- it("the inner effect is cleaned up when stop is called (no leak)", () => {
146
- vi.useFakeTimers();
147
- const s = signal(0);
148
- const d = debounced(s, 100);
149
-
150
- s.set(1);
151
- // Stop the debounced effect before the timer fires
152
- d.stop();
153
-
154
- // Advance time — the timer should not fire and update the signal
155
- vi.advanceTimersByTime(200);
156
- expect(d()).toBe(0); // still initial value, effect was stopped
157
-
158
- vi.useRealTimers();
159
- });
160
- });
161
-
162
- // ---------- history ----------
163
-
164
- describe("history", () => {
165
- it("current reflects signal value", () => {
166
- const s = signal("a");
167
- const h = history(s);
168
- expect(h.current()).toBe("a");
169
- });
170
-
171
- it("canUndo is false initially", () => {
172
- const s = signal(0);
173
- const h = history(s);
174
- expect(h.canUndo()).toBe(false);
175
- });
176
-
177
- it("canUndo becomes true after a change", () => {
178
- const s = signal(0);
179
- const h = history(s);
180
- s.set(1);
181
- expect(h.canUndo()).toBe(true);
182
- });
183
-
184
- it("undo reverts to previous value", () => {
185
- const s = signal(0);
186
- const h = history(s);
187
- s.set(1);
188
- s.set(2);
189
- h.undo();
190
- expect(h.current()).toBe(1);
191
- expect(s()).toBe(1);
192
- });
193
-
194
- it("redo re-applies reverted value", () => {
195
- const s = signal(0);
196
- const h = history(s);
197
- s.set(1);
198
- h.undo();
199
- expect(h.canRedo()).toBe(true);
200
- h.redo();
201
- expect(h.current()).toBe(1);
202
- expect(s()).toBe(1);
203
- });
204
-
205
- it("values() returns full timeline", () => {
206
- const s = signal("a");
207
- const h = history(s);
208
- s.set("b");
209
- s.set("c");
210
- expect(h.values()).toEqual(["a", "b", "c"]);
211
- });
212
-
213
- it("clear removes past and future", () => {
214
- const s = signal(0);
215
- const h = history(s);
216
- s.set(1);
217
- s.set(2);
218
- h.clear();
219
- expect(h.canUndo()).toBe(false);
220
- expect(h.canRedo()).toBe(false);
221
- });
222
-
223
- it("undo does nothing when there is no history", () => {
224
- const s = signal(5);
225
- const h = history(s);
226
- h.undo();
227
- expect(h.current()).toBe(5);
228
- });
229
-
230
- it("redo does nothing when there is no future", () => {
231
- const s = signal(5);
232
- const h = history(s);
233
- s.set(10);
234
- h.redo();
235
- expect(h.current()).toBe(10);
236
- });
237
-
238
- it("works with computed source (read-only — no signal.set)", async () => {
239
- const s = signal(1);
240
- const c = computed(() => s() * 2);
241
- const h = history(c);
242
- s.set(2);
243
- await Promise.resolve();
244
- expect(h.current()).toBe(4);
245
- expect(h.canUndo()).toBe(true);
246
- });
247
-
248
- it("respects limit — oldest entries are dropped", () => {
249
- const s = signal(0);
250
- const h = history(s, 3);
251
- for (let i = 1; i <= 5; i++) s.set(i);
252
- // past holds at most 3, plus current = 4 values total
253
- expect(h.values().length).toBeLessThanOrEqual(4);
254
- });
255
-
256
- it("limit=1 allows only one past entry", () => {
257
- const s = signal(0);
258
- const h = history(s, 1);
259
- s.set(1);
260
- s.set(2);
261
- s.set(3);
262
- // values = [past(1 entry max), current] → at most 2 entries
263
- expect(h.values().length).toBeLessThanOrEqual(2);
264
- expect(h.current()).toBe(3);
265
- });
266
-
267
- it("clear() after undo removes both past and future", () => {
268
- const s = signal(0);
269
- const h = history(s);
270
- s.set(1);
271
- s.set(2);
272
- h.undo();
273
- expect(h.canRedo()).toBe(true);
274
- h.clear();
275
- expect(h.canUndo()).toBe(false);
276
- expect(h.canRedo()).toBe(false);
277
- });
278
-
279
- it("undo() called more times than history entries — state stays consistent", () => {
280
- const s = signal(0);
281
- const h = history(s);
282
- s.set(1);
283
- h.undo();
284
- // No more history — undo is a no-op
285
- h.undo();
286
- h.undo();
287
- expect(h.current()).toBe(0);
288
- expect(h.canUndo()).toBe(false);
289
- });
290
-
291
- it("values() returns a snapshot — each call after source change yields a fresh array", () => {
292
- const s = signal("a");
293
- const h = history(s);
294
- s.set("b");
295
- s.set("c");
296
- const snapshot1 = h.values();
297
- expect(snapshot1).toEqual(["a", "b", "c"]);
298
- // Trigger a change so the computed re-evaluates on next read
299
- s.set("d");
300
- const snapshot2 = h.values();
301
- expect(snapshot2).toEqual(["a", "b", "c", "d"]);
302
- // snapshot1 and snapshot2 are different array instances
303
- expect(snapshot1).not.toBe(snapshot2);
304
- });
305
- });
@@ -1,61 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
-
4
- import { resource } from "../async/resource";
5
- import { _clearCache } from "../async/resource-cache";
6
-
7
- describe("resource — refetchOnFocus", () => {
8
- beforeEach(() => { _clearCache(); });
9
-
10
- it("refetches when document becomes visible", async () => {
11
- let call = 0;
12
- const fetcher = vi.fn(() => Promise.resolve(++call));
13
- const r = resource(fetcher, { refetchOnFocus: true });
14
- await vi.waitFor(() => r.status() === "success");
15
- expect(fetcher).toHaveBeenCalledTimes(1);
16
-
17
- Object.defineProperty(document, "visibilityState", {
18
- configurable: true,
19
- get: () => "visible",
20
- });
21
- document.dispatchEvent(new Event("visibilitychange"));
22
-
23
- await vi.waitFor(() => r.data() === 2);
24
- expect(fetcher).toHaveBeenCalledTimes(2);
25
- r.destroy();
26
- });
27
-
28
- it("does not refetch when document becomes hidden", async () => {
29
- const fetcher = vi.fn(() => Promise.resolve(1));
30
- const r = resource(fetcher, { refetchOnFocus: true });
31
- await vi.waitFor(() => r.status() === "success");
32
-
33
- Object.defineProperty(document, "visibilityState", {
34
- configurable: true,
35
- get: () => "hidden",
36
- });
37
- document.dispatchEvent(new Event("visibilitychange"));
38
- await new Promise((res) => setTimeout(res, 10));
39
-
40
- expect(fetcher).toHaveBeenCalledTimes(1);
41
- r.destroy();
42
- });
43
-
44
- it("destroy() removes the visibilitychange listener", async () => {
45
- let call = 0;
46
- const fetcher = vi.fn(() => Promise.resolve(++call));
47
- const r = resource(fetcher, { refetchOnFocus: true });
48
- await vi.waitFor(() => r.status() === "success");
49
-
50
- r.destroy();
51
-
52
- Object.defineProperty(document, "visibilityState", {
53
- configurable: true,
54
- get: () => "visible",
55
- });
56
- document.dispatchEvent(new Event("visibilitychange"));
57
- await new Promise((res) => setTimeout(res, 10));
58
-
59
- expect(fetcher).toHaveBeenCalledTimes(1); // no refetch after destroy
60
- });
61
- });
@@ -1,90 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
-
3
- import { flushPendingResources, resource } from "../async/resource";
4
- import { _clearCache, getPendingResources } from "../async/resource-cache";
5
- import { setServerRenderPass, isServerRenderPass } from "../async/server-mode";
6
-
7
- describe("server render pass — flushPendingResources", () => {
8
- beforeEach(() => { _clearCache(); });
9
- afterEach(() => { setServerRenderPass(false); });
10
-
11
- it("is a no-op when no server pass is active", async () => {
12
- setServerRenderPass(false);
13
- const r = resource(() => new Promise<string>(() => {}));
14
- expect(getPendingResources().size).toBe(0);
15
- await flushPendingResources();
16
- // Untouched — resource() never settled and flush didn't wait for it.
17
- expect(r.status()).toBe("pending");
18
- });
19
-
20
- it("resolves immediately when nothing is pending", async () => {
21
- setServerRenderPass(true);
22
- await expect(flushPendingResources()).resolves.toBeUndefined();
23
- });
24
-
25
- it("tracks isServerRenderPass() state via the setter", () => {
26
- expect(isServerRenderPass()).toBe(false);
27
- setServerRenderPass(true);
28
- expect(isServerRenderPass()).toBe(true);
29
- });
30
-
31
- it("awaits an unkeyed resource() (e.g. @Collection) before returning", async () => {
32
- setServerRenderPass(true);
33
- const r = resource(() => new Promise<string>((res) => { setTimeout(() => { res("content"); }, 5); }));
34
- await flushPendingResources();
35
- expect(r.status()).toBe("success");
36
- expect(r.data()).toBe("content");
37
- });
38
-
39
- it("awaits a keyed resource() too", async () => {
40
- setServerRenderPass(true);
41
- const r = resource(() => Promise.resolve("keyed-value"), { key: "ssg-key" });
42
- await flushPendingResources();
43
- expect(r.status()).toBe("success");
44
- expect(r.data()).toBe("keyed-value");
45
- });
46
-
47
- it("awaits multiple concurrently pending resources", async () => {
48
- setServerRenderPass(true);
49
- const r1 = resource(() => new Promise<number>((res) => { setTimeout(() => { res(1); }, 5); }));
50
- const r2 = resource(() => new Promise<number>((res) => { setTimeout(() => { res(2); }, 10); }));
51
- await flushPendingResources();
52
- expect(r1.data()).toBe(1);
53
- expect(r2.data()).toBe(2);
54
- });
55
-
56
- it("fixed-point loop: awaits a resource created while an earlier one settles", async () => {
57
- setServerRenderPass(true);
58
- let second: ReturnType<typeof resource> | undefined;
59
-
60
- // Simulates a component that, upon its first resource resolving, creates a
61
- // second resource (e.g. a detail page fetching related content) — the new
62
- // promise must still be picked up even though it didn't exist yet when
63
- // flushPendingResources() started its first pass.
64
- const first = resource(() => new Promise<string>((res) => {
65
- setTimeout(() => {
66
- res("first");
67
- second = resource(() => new Promise<string>((res2) => { setTimeout(() => { res2("second"); }, 5); }));
68
- }, 5);
69
- }));
70
-
71
- await flushPendingResources();
72
- expect(first.status()).toBe("success");
73
- expect(second?.status()).toBe("success");
74
- expect(second?.data()).toBe("second");
75
- });
76
-
77
- it("does not leave stale entries after resources settle", async () => {
78
- setServerRenderPass(true);
79
- resource(() => Promise.resolve("done"));
80
- await flushPendingResources();
81
- expect(getPendingResources().size).toBe(0);
82
- });
83
-
84
- it("a rejected resource does not hang the flush", async () => {
85
- setServerRenderPass(true);
86
- const r = resource(() => Promise.reject(new Error("boom")));
87
- await expect(flushPendingResources()).resolves.toBeUndefined();
88
- expect(r.status()).toBe("error");
89
- });
90
- });