@alepha/react 0.14.2 → 0.14.3
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/dist/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/head/index.d.ts +17 -17
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +2 -1
- package/dist/head/index.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -2
- package/dist/router/index.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +2 -1
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/router/providers/ReactServerProvider.ts +4 -3
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { AlephaDateTime } from "alepha/datetime";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { describe, test, vi } from "vitest";
|
|
6
|
+
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
7
|
+
import { useAction } from "./useAction.ts";
|
|
8
|
+
|
|
9
|
+
describe("useAction", () => {
|
|
10
|
+
test("should handle successful action", async ({ expect }) => {
|
|
11
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
12
|
+
await alepha.start();
|
|
13
|
+
|
|
14
|
+
const mockAction = vi.fn(async (value: string, ctx) => {
|
|
15
|
+
return `result: ${value}`;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
19
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const { result } = renderHook(
|
|
23
|
+
() => useAction({ handler: mockAction }, []),
|
|
24
|
+
{
|
|
25
|
+
wrapper,
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const action = result.current;
|
|
30
|
+
|
|
31
|
+
expect(action.loading).toBe(false);
|
|
32
|
+
expect(action.error).toBe(undefined);
|
|
33
|
+
expect(action.cancel).toBeDefined();
|
|
34
|
+
expect(action.run).toBeDefined();
|
|
35
|
+
|
|
36
|
+
const actionResult = await action.run("test");
|
|
37
|
+
|
|
38
|
+
expect(actionResult).toBe("result: test");
|
|
39
|
+
expect(mockAction).toHaveBeenCalledWith(
|
|
40
|
+
"test",
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
signal: expect.any(AbortSignal),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(result.current.loading).toBe(false);
|
|
47
|
+
expect(result.current.error).toBe(undefined);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("should emit react:action events", async ({ expect }) => {
|
|
51
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
52
|
+
await alepha.start();
|
|
53
|
+
|
|
54
|
+
const events: string[] = [];
|
|
55
|
+
|
|
56
|
+
alepha.events.on("react:action:begin", () => {
|
|
57
|
+
events.push("begin");
|
|
58
|
+
});
|
|
59
|
+
alepha.events.on("react:action:success", () => {
|
|
60
|
+
events.push("success");
|
|
61
|
+
});
|
|
62
|
+
alepha.events.on("react:action:end", () => {
|
|
63
|
+
events.push("end");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const mockAction = vi.fn(async (ctx) => "done");
|
|
67
|
+
|
|
68
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
69
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(
|
|
73
|
+
() => useAction({ handler: mockAction }, []),
|
|
74
|
+
{
|
|
75
|
+
wrapper,
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const action = result.current;
|
|
80
|
+
await action.run();
|
|
81
|
+
|
|
82
|
+
expect(events).toEqual(["begin", "success", "end"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test.skip("should handle errors", async ({ expect }) => {
|
|
86
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
87
|
+
await alepha.start();
|
|
88
|
+
|
|
89
|
+
const error = new Error("Test error");
|
|
90
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
91
|
+
throw error;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
95
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(
|
|
99
|
+
() => useAction({ handler: mockAction }, []),
|
|
100
|
+
{
|
|
101
|
+
wrapper,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const action = result.current;
|
|
106
|
+
|
|
107
|
+
await expect(() => action.run()).rejects.toThrow("Test error");
|
|
108
|
+
|
|
109
|
+
expect(result.current.error).toBe(error);
|
|
110
|
+
expect(result.current.loading).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("should emit react:action:error on failure", async ({ expect }) => {
|
|
114
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
115
|
+
await alepha.start();
|
|
116
|
+
|
|
117
|
+
const events: Array<{ type: string; error?: Error }> = [];
|
|
118
|
+
|
|
119
|
+
alepha.events.on("react:action:error", (ev) => {
|
|
120
|
+
events.push({ type: "error", error: ev.error });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const error = new Error("Test error");
|
|
124
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
129
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const { result } = renderHook(
|
|
133
|
+
() => useAction({ handler: mockAction }, []),
|
|
134
|
+
{
|
|
135
|
+
wrapper,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const action = result.current;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await action.run();
|
|
143
|
+
} catch {
|
|
144
|
+
// Expected
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(events).toHaveLength(1);
|
|
148
|
+
expect(events[0].type).toBe("error");
|
|
149
|
+
expect(events[0].error).toBe(error);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("should call custom error handler", async ({ expect }) => {
|
|
153
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
154
|
+
await alepha.start();
|
|
155
|
+
|
|
156
|
+
const error = new Error("Test error");
|
|
157
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
158
|
+
throw error;
|
|
159
|
+
});
|
|
160
|
+
const onError = vi.fn();
|
|
161
|
+
|
|
162
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
163
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const { result } = renderHook(
|
|
167
|
+
() => useAction({ handler: mockAction, onError }, []),
|
|
168
|
+
{ wrapper },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const action = result.current;
|
|
172
|
+
await action.run();
|
|
173
|
+
|
|
174
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("should call custom success handler", async ({ expect }) => {
|
|
178
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
179
|
+
await alepha.start();
|
|
180
|
+
|
|
181
|
+
const mockAction = vi.fn(async (ctx) => "result");
|
|
182
|
+
const onSuccess = vi.fn();
|
|
183
|
+
|
|
184
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
185
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook(
|
|
189
|
+
() => useAction({ handler: mockAction, onSuccess }, []),
|
|
190
|
+
{ wrapper },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const action = result.current;
|
|
194
|
+
await action.run();
|
|
195
|
+
|
|
196
|
+
expect(onSuccess).toHaveBeenCalledWith("result");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("should include action id in events", async ({ expect }) => {
|
|
200
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
201
|
+
await alepha.start();
|
|
202
|
+
|
|
203
|
+
const events: Array<{ id?: string }> = [];
|
|
204
|
+
|
|
205
|
+
alepha.events.on("react:action:begin", (ev) => {
|
|
206
|
+
events.push({ id: ev.id });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const mockAction = vi.fn(async (ctx) => "done");
|
|
210
|
+
|
|
211
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
212
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const { result } = renderHook(
|
|
216
|
+
() => useAction({ handler: mockAction, id: "test-action" }, []),
|
|
217
|
+
{ wrapper },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const action = result.current;
|
|
221
|
+
await action.run();
|
|
222
|
+
|
|
223
|
+
expect(events[0].id).toBe("test-action");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("should prevent concurrent executions", async ({ expect }) => {
|
|
227
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
228
|
+
await alepha.start();
|
|
229
|
+
|
|
230
|
+
let executionCount = 0;
|
|
231
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
232
|
+
executionCount++;
|
|
233
|
+
// Simulate slow operation
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
235
|
+
return executionCount;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
239
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const { result } = renderHook(
|
|
243
|
+
() => useAction({ handler: mockAction }, []),
|
|
244
|
+
{ wrapper },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const action = result.current;
|
|
248
|
+
|
|
249
|
+
// Call 100 times rapidly
|
|
250
|
+
const promises = Array.from({ length: 100 }, () => action.run());
|
|
251
|
+
|
|
252
|
+
// Wait for all to complete
|
|
253
|
+
await Promise.all(promises);
|
|
254
|
+
|
|
255
|
+
// Should have only executed once
|
|
256
|
+
expect(mockAction).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(executionCount).toBe(1);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should debounce action calls", async ({ expect }) => {
|
|
261
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
262
|
+
await alepha.start();
|
|
263
|
+
|
|
264
|
+
const mockAction = vi.fn(async (value: string, ctx) => value);
|
|
265
|
+
|
|
266
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
267
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const { result } = renderHook(
|
|
271
|
+
() => useAction({ handler: mockAction, debounce: 50 }, []),
|
|
272
|
+
{ wrapper },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const action = result.current;
|
|
276
|
+
|
|
277
|
+
// Call 100 times rapidly with different values
|
|
278
|
+
for (let i = 0; i < 100; i++) {
|
|
279
|
+
action.run(`value-${i}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Wait for debounce to complete
|
|
283
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
284
|
+
|
|
285
|
+
// Should have only executed once with the last value
|
|
286
|
+
expect(mockAction).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(mockAction).toHaveBeenCalledWith("value-99", expect.any(Object));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("should reset debounce timer on each call", async ({ expect }) => {
|
|
291
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
292
|
+
await alepha.start();
|
|
293
|
+
|
|
294
|
+
const mockAction = vi.fn(async (value: string, ctx) => value);
|
|
295
|
+
|
|
296
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
297
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const { result } = renderHook(
|
|
301
|
+
() => useAction({ handler: mockAction, debounce: 50 }, []),
|
|
302
|
+
{ wrapper },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const action = result.current;
|
|
306
|
+
|
|
307
|
+
// Call multiple times with delays
|
|
308
|
+
action.run("first");
|
|
309
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
310
|
+
action.run("second");
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
312
|
+
action.run("third");
|
|
313
|
+
|
|
314
|
+
// Wait for final debounce
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
316
|
+
|
|
317
|
+
// Should have only executed once with the last value
|
|
318
|
+
expect(mockAction).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect(mockAction).toHaveBeenCalledWith("third", expect.any(Object));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("should pass AbortSignal to handler", async ({ expect }) => {
|
|
323
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
324
|
+
await alepha.start();
|
|
325
|
+
|
|
326
|
+
let receivedSignal: AbortSignal | undefined;
|
|
327
|
+
|
|
328
|
+
const mockAction = vi.fn(async (value: string, { signal }) => {
|
|
329
|
+
receivedSignal = signal as AbortSignal;
|
|
330
|
+
return value;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
334
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const { result } = renderHook(
|
|
338
|
+
() => useAction({ handler: mockAction }, []),
|
|
339
|
+
{ wrapper },
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const action = result.current;
|
|
343
|
+
await action.run("test");
|
|
344
|
+
|
|
345
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal);
|
|
346
|
+
expect(receivedSignal?.aborted).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("should cancel in-flight request with cancel()", async ({ expect }) => {
|
|
350
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
351
|
+
await alepha.start();
|
|
352
|
+
|
|
353
|
+
let wasAborted = false;
|
|
354
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
356
|
+
wasAborted = ctx.signal.aborted;
|
|
357
|
+
return "done";
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
361
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const { result } = renderHook(
|
|
365
|
+
() => useAction({ handler: mockAction }, []),
|
|
366
|
+
{ wrapper },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const action = result.current;
|
|
370
|
+
|
|
371
|
+
// Start action
|
|
372
|
+
const promise = action.run();
|
|
373
|
+
|
|
374
|
+
// Cancel after 50ms
|
|
375
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
376
|
+
action.cancel();
|
|
377
|
+
|
|
378
|
+
await promise;
|
|
379
|
+
|
|
380
|
+
expect(wasAborted).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("should cancel debounced action", async ({ expect }) => {
|
|
384
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
385
|
+
await alepha.start();
|
|
386
|
+
|
|
387
|
+
const mockAction = vi.fn(async (value: string, ctx) => value);
|
|
388
|
+
|
|
389
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
390
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const { result } = renderHook(
|
|
394
|
+
() => useAction({ handler: mockAction, debounce: 100 }, []),
|
|
395
|
+
{ wrapper },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const action = result.current;
|
|
399
|
+
|
|
400
|
+
// Call handler
|
|
401
|
+
action.run("test");
|
|
402
|
+
|
|
403
|
+
// Cancel before debounce completes
|
|
404
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
405
|
+
action.cancel();
|
|
406
|
+
|
|
407
|
+
// Wait for debounce period
|
|
408
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
409
|
+
|
|
410
|
+
// Should not have executed
|
|
411
|
+
expect(mockAction).not.toHaveBeenCalled();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("should handle AbortError gracefully", async ({ expect }) => {
|
|
415
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
416
|
+
await alepha.start();
|
|
417
|
+
|
|
418
|
+
const mockAction = vi.fn(async (ctx) => {
|
|
419
|
+
const abortError = new Error("The operation was aborted");
|
|
420
|
+
abortError.name = "AbortError";
|
|
421
|
+
throw abortError;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
425
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const { result } = renderHook(
|
|
429
|
+
() => useAction({ handler: mockAction }, []),
|
|
430
|
+
{ wrapper },
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const action = result.current;
|
|
434
|
+
await action.run();
|
|
435
|
+
|
|
436
|
+
// Should not set error state for abort errors
|
|
437
|
+
expect(result.current.error).toBe(undefined);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("should run action on mount with runOnInit", async ({ expect }) => {
|
|
441
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
442
|
+
await alepha.start();
|
|
443
|
+
|
|
444
|
+
const mockAction = vi.fn(async (ctx) => "initialized");
|
|
445
|
+
|
|
446
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
447
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
renderHook(() => useAction({ handler: mockAction, runOnInit: true }, []), {
|
|
451
|
+
wrapper,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Wait a tick for the effect to run
|
|
455
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
456
|
+
|
|
457
|
+
// Should have been called once on mount
|
|
458
|
+
expect(mockAction).toHaveBeenCalledTimes(1);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("should not run action on mount without runOnInit", async ({
|
|
462
|
+
expect,
|
|
463
|
+
}) => {
|
|
464
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
465
|
+
await alepha.start();
|
|
466
|
+
|
|
467
|
+
const mockAction = vi.fn(async (ctx) => "result");
|
|
468
|
+
|
|
469
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
470
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
renderHook(() => useAction({ handler: mockAction }, []), {
|
|
474
|
+
wrapper,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Wait a tick
|
|
478
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
479
|
+
|
|
480
|
+
// Should not have been called
|
|
481
|
+
expect(mockAction).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("should run action periodically with runEvery (milliseconds)", async ({
|
|
485
|
+
expect,
|
|
486
|
+
}) => {
|
|
487
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
488
|
+
await alepha.start();
|
|
489
|
+
|
|
490
|
+
const mockAction = vi.fn(async (ctx) => "fired");
|
|
491
|
+
|
|
492
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
493
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const { unmount } = renderHook(
|
|
497
|
+
() => useAction({ handler: mockAction, runEvery: 60 }, []),
|
|
498
|
+
{ wrapper },
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Wait for multiple intervals
|
|
502
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
503
|
+
|
|
504
|
+
// Should have been called approximately 3 times (at 50ms, 100ms, 150ms)
|
|
505
|
+
expect(mockAction.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
506
|
+
expect(mockAction.mock.calls.length).toBeLessThanOrEqual(4);
|
|
507
|
+
|
|
508
|
+
// Cleanup
|
|
509
|
+
unmount();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("should run action periodically with runEvery (duration tuple)", async ({
|
|
513
|
+
expect,
|
|
514
|
+
}) => {
|
|
515
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
516
|
+
await alepha.start();
|
|
517
|
+
|
|
518
|
+
const mockAction = vi.fn(async (ctx) => "polled");
|
|
519
|
+
|
|
520
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
521
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const { unmount } = renderHook(
|
|
525
|
+
() =>
|
|
526
|
+
useAction({ handler: mockAction, runEvery: [50, "milliseconds"] }, []),
|
|
527
|
+
{ wrapper },
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// Wait for multiple intervals
|
|
531
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
532
|
+
|
|
533
|
+
// Should have been called approximately 3 times
|
|
534
|
+
expect(mockAction.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
535
|
+
expect(mockAction.mock.calls.length).toBeLessThanOrEqual(4);
|
|
536
|
+
|
|
537
|
+
// Cleanup
|
|
538
|
+
unmount();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("should cleanup interval on unmount", async ({ expect }) => {
|
|
542
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
543
|
+
await alepha.start();
|
|
544
|
+
|
|
545
|
+
const mockAction = vi.fn(async (ctx) => "polled");
|
|
546
|
+
|
|
547
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
548
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const { unmount } = renderHook(
|
|
552
|
+
() => useAction({ handler: mockAction, runEvery: 50 }, []),
|
|
553
|
+
{ wrapper },
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// Wait for one interval
|
|
557
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
558
|
+
const callsBeforeUnmount = mockAction.mock.calls.length;
|
|
559
|
+
|
|
560
|
+
// Unmount
|
|
561
|
+
unmount();
|
|
562
|
+
|
|
563
|
+
// Wait for what would be another interval
|
|
564
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
565
|
+
|
|
566
|
+
// Should not have been called again after unmount
|
|
567
|
+
expect(mockAction.mock.calls.length).toBe(callsBeforeUnmount);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
@@ -127,6 +127,7 @@ export function useAction<Args extends any[], Result = void>(
|
|
|
127
127
|
const dateTimeProvider = useInject(DateTimeProvider);
|
|
128
128
|
const [loading, setLoading] = useState(false);
|
|
129
129
|
const [error, setError] = useState<Error | undefined>();
|
|
130
|
+
const [result, setResult] = useState<Result | undefined>();
|
|
130
131
|
const isExecutingRef = useRef(false);
|
|
131
132
|
const debounceTimerRef = useRef<Timeout | undefined>(undefined);
|
|
132
133
|
const abortControllerRef = useRef<AbortController | undefined>(undefined);
|
|
@@ -189,6 +190,9 @@ export function useAction<Args extends any[], Result = void>(
|
|
|
189
190
|
signal: abortController.signal,
|
|
190
191
|
} as any);
|
|
191
192
|
|
|
193
|
+
// TODO: it should be after onSuccess?
|
|
194
|
+
setResult(result as Result);
|
|
195
|
+
|
|
192
196
|
// Only update state if still mounted and not aborted
|
|
193
197
|
if (!isMountedRef.current || abortController.signal.aborted) {
|
|
194
198
|
return;
|
|
@@ -203,6 +207,7 @@ export function useAction<Args extends any[], Result = void>(
|
|
|
203
207
|
await options.onSuccess(result);
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
|
|
206
211
|
return result;
|
|
207
212
|
} catch (err) {
|
|
208
213
|
// Ignore abort errors
|
|
@@ -327,6 +332,7 @@ export function useAction<Args extends any[], Result = void>(
|
|
|
327
332
|
loading,
|
|
328
333
|
error,
|
|
329
334
|
cancel,
|
|
335
|
+
result,
|
|
330
336
|
};
|
|
331
337
|
}
|
|
332
338
|
|
|
@@ -467,4 +473,9 @@ export interface UseActionReturn<Args extends any[], Result> {
|
|
|
467
473
|
* ```
|
|
468
474
|
*/
|
|
469
475
|
cancel: () => void;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The result data from the last successful action execution.
|
|
479
|
+
*/
|
|
480
|
+
result?: Result;
|
|
470
481
|
}
|