@cplieger/actions 1.0.1 → 1.1.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.
- package/jsr.json +1 -1
- package/package.json +6 -2
- package/src/__test-helpers__/action-test-setup.ts +0 -34
- package/src/api-idempotency.test.ts +0 -163
- package/src/api.test.ts +0 -96
- package/src/cleanup.test.ts +0 -102
- package/src/configure-api.test.ts +0 -219
- package/src/cross-action-coordination.test.ts +0 -176
- package/src/cross-action-races-extra.test.ts +0 -231
- package/src/cross-action-races.test.ts +0 -213
- package/src/cross-action-scope-serial.test.ts +0 -121
- package/src/debounce.test.ts +0 -121
- package/src/define.property.test.ts +0 -133
- package/src/define.test.ts +0 -476
- package/src/dispatch-callbacks.test.ts +0 -141
- package/src/dispatch-options.test.ts +0 -197
- package/src/edge-cases.test.ts +0 -81
- package/src/error.test.ts +0 -268
- package/src/leak-stress.test.ts +0 -117
- package/src/loading.test.ts +0 -146
- package/src/new-features.test.ts +0 -246
- package/src/poll.test.ts +0 -385
- package/src/primitive-edge-cases.test.ts +0 -105
- package/src/primitive-interactions.test.ts +0 -112
- package/src/primitives.test.ts +0 -215
- package/src/race-dedupe-cancel.test.ts +0 -88
- package/src/redteam.test.ts +0 -254
- package/src/redteam2.test.ts +0 -337
- package/src/redteam3.test.ts +0 -454
- package/src/registry.test.ts +0 -271
- package/src/retry-network-delay.test.ts +0 -155
- package/src/transport.test.ts +0 -139
package/src/poll.test.ts
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
// Tests for pollAction.
|
|
3
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
4
|
-
|
|
5
|
-
vi.mock("./notifier.js", () => ({
|
|
6
|
-
configure: vi.fn(),
|
|
7
|
-
notifySuccess: vi.fn(),
|
|
8
|
-
notifyError: vi.fn(),
|
|
9
|
-
_resetNotifierForTest: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { defineAction, _resetForTest as resetDefine } from "./define.js";
|
|
13
|
-
import { _resetForTest as resetRegistry } from "./registry.js";
|
|
14
|
-
import { _resetForTest as resetCleanup, _cancelAllForTest as cancelAllForTest } from "./cleanup.js";
|
|
15
|
-
import { pollAction } from "./poll.js";
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
resetDefine();
|
|
19
|
-
resetRegistry();
|
|
20
|
-
resetCleanup();
|
|
21
|
-
vi.clearAllMocks();
|
|
22
|
-
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
23
|
-
Object.defineProperty(document, "visibilityState", { value: "visible", configurable: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
vi.useRealTimers();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("pollAction — basic scheduling", () => {
|
|
31
|
-
it("dispatches immediately on start, then at the interval", async () => {
|
|
32
|
-
let count = 0;
|
|
33
|
-
const action = defineAction<undefined, number>({
|
|
34
|
-
name: "test.poll.basic",
|
|
35
|
-
run: async () => ++count,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
vi.useFakeTimers();
|
|
39
|
-
const stop = pollAction(action, undefined, { interval: 1000 });
|
|
40
|
-
|
|
41
|
-
await vi.runAllTicks();
|
|
42
|
-
await Promise.resolve();
|
|
43
|
-
await Promise.resolve();
|
|
44
|
-
expect(count).toBe(1);
|
|
45
|
-
|
|
46
|
-
await vi.advanceTimersByTimeAsync(1000);
|
|
47
|
-
expect(count).toBe(2);
|
|
48
|
-
|
|
49
|
-
await vi.advanceTimersByTimeAsync(1000);
|
|
50
|
-
expect(count).toBe(3);
|
|
51
|
-
|
|
52
|
-
stop();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("stop() cancels the next scheduled poll", async () => {
|
|
56
|
-
let count = 0;
|
|
57
|
-
const action = defineAction<undefined, number>({
|
|
58
|
-
name: "test.poll.stop",
|
|
59
|
-
run: async () => ++count,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
vi.useFakeTimers();
|
|
63
|
-
const stop = pollAction(action, undefined, { interval: 100 });
|
|
64
|
-
await Promise.resolve();
|
|
65
|
-
expect(count).toBe(1);
|
|
66
|
-
|
|
67
|
-
stop();
|
|
68
|
-
await vi.advanceTimersByTimeAsync(500);
|
|
69
|
-
expect(count).toBe(1);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("stop() is idempotent", () => {
|
|
73
|
-
const action = defineAction<undefined, undefined>({
|
|
74
|
-
name: "test.poll.idempotent",
|
|
75
|
-
run: async () => undefined,
|
|
76
|
-
});
|
|
77
|
-
const stop = pollAction(action, undefined, { interval: 1000 });
|
|
78
|
-
expect(() => {
|
|
79
|
-
stop();
|
|
80
|
-
stop();
|
|
81
|
-
stop();
|
|
82
|
-
}).not.toThrow();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("pollAction — pauseWhenHidden", () => {
|
|
87
|
-
it("pauses on visibilitychange to hidden, resumes on visible with immediate dispatch", async () => {
|
|
88
|
-
let count = 0;
|
|
89
|
-
const action = defineAction<undefined, number>({
|
|
90
|
-
name: "test.poll.hidden",
|
|
91
|
-
run: async () => ++count,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
vi.useFakeTimers();
|
|
95
|
-
const stop = pollAction(action, undefined, { interval: 1000, pauseWhenHidden: true });
|
|
96
|
-
await Promise.resolve();
|
|
97
|
-
expect(count).toBe(1);
|
|
98
|
-
|
|
99
|
-
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
100
|
-
document.dispatchEvent(new Event("visibilitychange"));
|
|
101
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
102
|
-
expect(count).toBe(1);
|
|
103
|
-
|
|
104
|
-
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
105
|
-
document.dispatchEvent(new Event("visibilitychange"));
|
|
106
|
-
await Promise.resolve();
|
|
107
|
-
await Promise.resolve();
|
|
108
|
-
expect(count).toBe(2);
|
|
109
|
-
|
|
110
|
-
stop();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("doesn't fire the first poll if started while hidden", async () => {
|
|
114
|
-
let count = 0;
|
|
115
|
-
const action = defineAction<undefined, number>({
|
|
116
|
-
name: "test.poll.start_hidden",
|
|
117
|
-
run: async () => ++count,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
121
|
-
vi.useFakeTimers();
|
|
122
|
-
const stop = pollAction(action, undefined, { interval: 1000, pauseWhenHidden: true });
|
|
123
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
124
|
-
expect(count).toBe(0);
|
|
125
|
-
|
|
126
|
-
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
127
|
-
document.dispatchEvent(new Event("visibilitychange"));
|
|
128
|
-
await Promise.resolve();
|
|
129
|
-
expect(count).toBe(1);
|
|
130
|
-
|
|
131
|
-
stop();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("pauseWhenHidden: false keeps polling while hidden", async () => {
|
|
135
|
-
let count = 0;
|
|
136
|
-
const action = defineAction<undefined, number>({
|
|
137
|
-
name: "test.poll.always",
|
|
138
|
-
run: async () => ++count,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
vi.useFakeTimers();
|
|
142
|
-
const stop = pollAction(action, undefined, { interval: 100, pauseWhenHidden: false });
|
|
143
|
-
await Promise.resolve();
|
|
144
|
-
|
|
145
|
-
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
146
|
-
document.dispatchEvent(new Event("visibilitychange"));
|
|
147
|
-
await vi.advanceTimersByTimeAsync(300);
|
|
148
|
-
expect(count).toBeGreaterThan(1);
|
|
149
|
-
|
|
150
|
-
stop();
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe("pollAction — refreshOnFocus", () => {
|
|
155
|
-
it("dispatches immediately on window focus", async () => {
|
|
156
|
-
let count = 0;
|
|
157
|
-
const action = defineAction<undefined, number>({
|
|
158
|
-
name: "test.poll.focus",
|
|
159
|
-
run: async () => ++count,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
vi.useFakeTimers();
|
|
163
|
-
const stop = pollAction(action, undefined, { interval: 10_000, refreshOnFocus: true });
|
|
164
|
-
|
|
165
|
-
await vi.runAllTicks();
|
|
166
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
167
|
-
expect(count).toBe(1);
|
|
168
|
-
|
|
169
|
-
window.dispatchEvent(new Event("focus"));
|
|
170
|
-
|
|
171
|
-
await vi.runAllTicks();
|
|
172
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
173
|
-
expect(count).toBe(2);
|
|
174
|
-
|
|
175
|
-
stop();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("refreshOnFocus: false ignores focus events", async () => {
|
|
179
|
-
let count = 0;
|
|
180
|
-
const action = defineAction<undefined, number>({
|
|
181
|
-
name: "test.poll.no_focus",
|
|
182
|
-
run: async () => ++count,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
vi.useFakeTimers();
|
|
186
|
-
const stop = pollAction(action, undefined, { interval: 10_000, refreshOnFocus: false });
|
|
187
|
-
|
|
188
|
-
await vi.runAllTicks();
|
|
189
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
190
|
-
expect(count).toBe(1);
|
|
191
|
-
|
|
192
|
-
window.dispatchEvent(new Event("focus"));
|
|
193
|
-
|
|
194
|
-
await vi.runAllTicks();
|
|
195
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
196
|
-
expect(count).toBe(1);
|
|
197
|
-
|
|
198
|
-
stop();
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("pollAction — backoffOnError", () => {
|
|
203
|
-
it("delays grow on consecutive failures", async () => {
|
|
204
|
-
let count = 0;
|
|
205
|
-
const action = defineAction<undefined, number>({
|
|
206
|
-
name: "test.poll.backoff",
|
|
207
|
-
run: async () => {
|
|
208
|
-
count++;
|
|
209
|
-
throw new Error("fail");
|
|
210
|
-
},
|
|
211
|
-
error: false,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
vi.useFakeTimers();
|
|
215
|
-
const stop = pollAction(action, undefined, {
|
|
216
|
-
interval: 100,
|
|
217
|
-
backoffOnError: { factor: 2, max: 10_000 },
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
await vi.runAllTicks();
|
|
221
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
222
|
-
const after1 = count;
|
|
223
|
-
expect(after1).toBeGreaterThanOrEqual(1);
|
|
224
|
-
|
|
225
|
-
await vi.advanceTimersByTimeAsync(1500);
|
|
226
|
-
expect(count).toBeGreaterThan(after1);
|
|
227
|
-
expect(count).toBeLessThan(10);
|
|
228
|
-
|
|
229
|
-
stop();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("caps the delay at max", async () => {
|
|
233
|
-
let count = 0;
|
|
234
|
-
const action = defineAction<undefined, number>({
|
|
235
|
-
name: "test.poll.cap",
|
|
236
|
-
run: async () => {
|
|
237
|
-
count++;
|
|
238
|
-
throw new Error("fail");
|
|
239
|
-
},
|
|
240
|
-
error: false,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
vi.useFakeTimers();
|
|
244
|
-
const stop = pollAction(action, undefined, {
|
|
245
|
-
interval: 100,
|
|
246
|
-
backoffOnError: { factor: 10, max: 500 },
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await vi.runAllTicks();
|
|
250
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
251
|
-
expect(count).toBeGreaterThanOrEqual(1);
|
|
252
|
-
|
|
253
|
-
await vi.advanceTimersByTimeAsync(2500);
|
|
254
|
-
expect(count).toBeGreaterThan(2);
|
|
255
|
-
expect(count).toBeLessThan(8);
|
|
256
|
-
|
|
257
|
-
stop();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("resets to base interval after a successful poll", async () => {
|
|
261
|
-
let fail = true;
|
|
262
|
-
let count = 0;
|
|
263
|
-
const action = defineAction<undefined, number>({
|
|
264
|
-
name: "test.poll.reset",
|
|
265
|
-
run: async () => {
|
|
266
|
-
count++;
|
|
267
|
-
if (fail) {
|
|
268
|
-
throw new Error("fail");
|
|
269
|
-
}
|
|
270
|
-
return count;
|
|
271
|
-
},
|
|
272
|
-
error: false,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const stop = pollAction(action, undefined, {
|
|
276
|
-
interval: 20,
|
|
277
|
-
backoffOnError: { factor: 4, max: 10_000 },
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
281
|
-
const failedCount = count;
|
|
282
|
-
expect(failedCount).toBeGreaterThanOrEqual(1);
|
|
283
|
-
|
|
284
|
-
fail = false;
|
|
285
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
286
|
-
const finalCount = count;
|
|
287
|
-
expect(finalCount).toBeGreaterThan(failedCount);
|
|
288
|
-
|
|
289
|
-
stop();
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("pollAction — onSuccess callback", () => {
|
|
294
|
-
it("invokes onSuccess with the result on each successful dispatch", async () => {
|
|
295
|
-
let count = 0;
|
|
296
|
-
const action = defineAction<undefined, number>({
|
|
297
|
-
name: "test.poll.onSuccess",
|
|
298
|
-
run: async () => ++count,
|
|
299
|
-
});
|
|
300
|
-
const onSuccess = vi.fn<(n: number) => void>();
|
|
301
|
-
|
|
302
|
-
const stop = pollAction(action, undefined, {
|
|
303
|
-
interval: 20,
|
|
304
|
-
onSuccess,
|
|
305
|
-
pauseWhenHidden: false,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
309
|
-
expect(onSuccess.mock.calls.length).toBeGreaterThan(1);
|
|
310
|
-
expect(onSuccess.mock.calls[0]?.[0]).toBe(1);
|
|
311
|
-
|
|
312
|
-
stop();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("does not invoke onSuccess on failed dispatch", async () => {
|
|
316
|
-
let count = 0;
|
|
317
|
-
const action = defineAction<undefined, number>({
|
|
318
|
-
name: "test.poll.onSuccess_fail",
|
|
319
|
-
run: async () => {
|
|
320
|
-
count++;
|
|
321
|
-
throw new Error("fail");
|
|
322
|
-
},
|
|
323
|
-
error: false,
|
|
324
|
-
});
|
|
325
|
-
const onSuccess = vi.fn<(n: number) => void>();
|
|
326
|
-
|
|
327
|
-
const stop = pollAction(action, undefined, {
|
|
328
|
-
interval: 20,
|
|
329
|
-
onSuccess,
|
|
330
|
-
pauseWhenHidden: false,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
334
|
-
expect(count).toBeGreaterThan(0);
|
|
335
|
-
expect(onSuccess).not.toHaveBeenCalled();
|
|
336
|
-
|
|
337
|
-
stop();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("catches errors thrown by onSuccess and continues polling", async () => {
|
|
341
|
-
let count = 0;
|
|
342
|
-
const action = defineAction<undefined, number>({
|
|
343
|
-
name: "test.poll.onSuccess_throws",
|
|
344
|
-
run: async () => ++count,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
348
|
-
const onSuccess = vi.fn<(n: number) => void>(() => {
|
|
349
|
-
throw new Error("kaboom");
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
const stop = pollAction(action, undefined, {
|
|
353
|
-
interval: 20,
|
|
354
|
-
onSuccess,
|
|
355
|
-
pauseWhenHidden: false,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
359
|
-
expect(count).toBeGreaterThan(1);
|
|
360
|
-
expect(consoleError).toHaveBeenCalled();
|
|
361
|
-
|
|
362
|
-
consoleError.mockRestore();
|
|
363
|
-
stop();
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
describe("pollAction — cleanup integration", () => {
|
|
368
|
-
it("auto-stops when registered cleanup fires (e.g. beforeunload)", async () => {
|
|
369
|
-
let count = 0;
|
|
370
|
-
const action = defineAction<undefined, number>({
|
|
371
|
-
name: "test.poll.cleanup",
|
|
372
|
-
run: async () => ++count,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
vi.useFakeTimers();
|
|
376
|
-
pollAction(action, undefined, { interval: 100 });
|
|
377
|
-
await Promise.resolve();
|
|
378
|
-
expect(count).toBe(1);
|
|
379
|
-
|
|
380
|
-
cancelAllForTest();
|
|
381
|
-
|
|
382
|
-
await vi.advanceTimersByTimeAsync(500);
|
|
383
|
-
expect(count).toBe(1);
|
|
384
|
-
});
|
|
385
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
-
vi.mock("./notifier.js", () => ({
|
|
4
|
-
configure: vi.fn(),
|
|
5
|
-
notifySuccess: vi.fn(),
|
|
6
|
-
notifyError: vi.fn(),
|
|
7
|
-
_resetNotifierForTest: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
import { defineAction, _resetForTest as resetDefine } from "./define.js";
|
|
10
|
-
import { _resetForTest as resetRegistry } from "./registry.js";
|
|
11
|
-
import { _resetForTest as resetCleanup } from "./cleanup.js";
|
|
12
|
-
import { ActionError, retryNetwork } from "./error.js";
|
|
13
|
-
import * as notifier from "./notifier.js";
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
resetDefine();
|
|
17
|
-
resetRegistry();
|
|
18
|
-
resetCleanup();
|
|
19
|
-
vi.clearAllMocks();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("dedupe with undefined args", () => {
|
|
23
|
-
it("dedupe: true collapses dispatches when args is undefined", async () => {
|
|
24
|
-
let runCalls = 0;
|
|
25
|
-
const action = defineAction<void, string>({
|
|
26
|
-
name: "test.dedupe_undefined_args",
|
|
27
|
-
dedupe: true,
|
|
28
|
-
run: () => {
|
|
29
|
-
runCalls++;
|
|
30
|
-
return new Promise<string>((r) =>
|
|
31
|
-
setTimeout(() => {
|
|
32
|
-
r("ok");
|
|
33
|
-
}, 10),
|
|
34
|
-
);
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
const p1 = action.dispatch(undefined);
|
|
38
|
-
const p2 = action.dispatch(undefined);
|
|
39
|
-
expect(runCalls).toBe(1);
|
|
40
|
-
const [r1, r2] = await Promise.all([p1, p2]);
|
|
41
|
-
expect(r1).toBe("ok");
|
|
42
|
-
expect(r2).toBe("ok");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("structuredClone fallback on retry (non-cloneable args)", () => {
|
|
47
|
-
it("retry button uses shallow copy when structuredClone fails (DOM refs)", async () => {
|
|
48
|
-
const el = { tagName: "BUTTON", focus: () => {} };
|
|
49
|
-
let lastArgs: { el: typeof el; label: string } | undefined;
|
|
50
|
-
const action = defineAction<{ el: typeof el; label: string }, string>({
|
|
51
|
-
name: "test.retry_dom_args",
|
|
52
|
-
run: async (args) => {
|
|
53
|
-
lastArgs = args;
|
|
54
|
-
throw new ActionError("fail", { status: 0 });
|
|
55
|
-
},
|
|
56
|
-
retryable: retryNetwork,
|
|
57
|
-
});
|
|
58
|
-
const args = { el, label: "Save" };
|
|
59
|
-
await action.dispatch(args);
|
|
60
|
-
args.label = "MUTATED";
|
|
61
|
-
const retryFn = vi.mocked(notifier.notifyError).mock.calls[0]?.[1]?.onClick as () => void;
|
|
62
|
-
expect(retryFn).toBeDefined();
|
|
63
|
-
retryFn();
|
|
64
|
-
await vi.waitFor(() => {
|
|
65
|
-
expect(lastArgs).toBeDefined();
|
|
66
|
-
});
|
|
67
|
-
expect(lastArgs!.label).toBe("Save");
|
|
68
|
-
expect(lastArgs!.el).toBe(el);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("cancel after dedupe entry created but before runOnce starts (scope-queued)", () => {
|
|
73
|
-
it("cancel while scope-queued resolves null without running", async () => {
|
|
74
|
-
let resolveOccupant: (() => void) | null = null;
|
|
75
|
-
let victimRan = false;
|
|
76
|
-
const occupant = defineAction<void, string>({
|
|
77
|
-
name: "test.scope_occ",
|
|
78
|
-
scope: "q",
|
|
79
|
-
run: () =>
|
|
80
|
-
new Promise<string>((r) => {
|
|
81
|
-
resolveOccupant = () => r("occ");
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
const victim = defineAction<void, string>({
|
|
85
|
-
name: "test.scope_victim",
|
|
86
|
-
scope: "q",
|
|
87
|
-
dedupe: true,
|
|
88
|
-
error: false,
|
|
89
|
-
run: () => {
|
|
90
|
-
victimRan = true;
|
|
91
|
-
return Promise.resolve("v");
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
const pOcc = occupant.dispatch();
|
|
95
|
-
await Promise.resolve();
|
|
96
|
-
const pVictim = victim.dispatch();
|
|
97
|
-
await Promise.resolve();
|
|
98
|
-
victim.cancel();
|
|
99
|
-
resolveOccupant!();
|
|
100
|
-
await pOcc;
|
|
101
|
-
const rVictim = await pVictim;
|
|
102
|
-
expect(rVictim).toBeNull();
|
|
103
|
-
expect(victimRan).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
-
vi.mock("./notifier.js", () => ({
|
|
4
|
-
configure: vi.fn(),
|
|
5
|
-
notifySuccess: vi.fn(),
|
|
6
|
-
notifyError: vi.fn(),
|
|
7
|
-
_resetNotifierForTest: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
import { defineAction, _resetForTest as resetDefine } from "./define.js";
|
|
10
|
-
import { _resetForTest as resetRegistry } from "./registry.js";
|
|
11
|
-
import { _resetForTest as resetCleanup } from "./cleanup.js";
|
|
12
|
-
import { debouncedDispatch } from "./debounce.js";
|
|
13
|
-
import { configureTransport, transportAction, _resetTransportForTest } from "./transport.js";
|
|
14
|
-
|
|
15
|
-
const mockSend = vi.fn();
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
resetDefine();
|
|
19
|
-
resetRegistry();
|
|
20
|
-
resetCleanup();
|
|
21
|
-
_resetTransportForTest();
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
configureTransport(mockSend);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("dedupe + cancel interaction", () => {
|
|
27
|
-
it("two concurrent dispatches with dedupe, then cancel — both resolve null", async () => {
|
|
28
|
-
let runCalls = 0;
|
|
29
|
-
const action = defineAction<{ id: string }, string>({
|
|
30
|
-
name: "test.dedupe_cancel",
|
|
31
|
-
dedupe: true,
|
|
32
|
-
run: (_args, signal) => {
|
|
33
|
-
runCalls++;
|
|
34
|
-
return new Promise<string>((_resolve, reject) => {
|
|
35
|
-
signal.addEventListener("abort", () => {
|
|
36
|
-
reject(new DOMException("aborted", "AbortError"));
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
const p1 = action.dispatch({ id: "a" });
|
|
42
|
-
const p2 = action.dispatch({ id: "a" });
|
|
43
|
-
expect(runCalls).toBe(1);
|
|
44
|
-
action.cancel();
|
|
45
|
-
const [r1, r2] = await Promise.all([p1, p2]);
|
|
46
|
-
expect(r1).toBeNull();
|
|
47
|
-
expect(r2).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("debouncedDispatch leading + flush interaction", () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
vi.useFakeTimers();
|
|
54
|
-
});
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
vi.useRealTimers();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("flush after leading fire dispatches the trailing args immediately", () => {
|
|
60
|
-
const runArgs: string[] = [];
|
|
61
|
-
const action = defineAction<string, void>({
|
|
62
|
-
name: "test.leading_flush",
|
|
63
|
-
run: (args) => {
|
|
64
|
-
runArgs.push(args);
|
|
65
|
-
return Promise.resolve();
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
const dbg = debouncedDispatch(action, { wait: 100, leading: true });
|
|
69
|
-
dbg("a");
|
|
70
|
-
expect(runArgs).toEqual(["a"]);
|
|
71
|
-
dbg("b");
|
|
72
|
-
dbg.flush();
|
|
73
|
-
expect(runArgs).toEqual(["a", "b"]);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("transportAction idempotency_key interaction", () => {
|
|
78
|
-
it("transportAction with idempotencyKey: true sends key in command", async () => {
|
|
79
|
-
mockSend.mockResolvedValue({ ok: true, status: 200 });
|
|
80
|
-
const action = transportAction<{ chatID: string }>({
|
|
81
|
-
name: "test.transport_idem",
|
|
82
|
-
idempotencyKey: true,
|
|
83
|
-
command: ({ chatID }) => ({ type: "send", chat_id: chatID }),
|
|
84
|
-
error: false,
|
|
85
|
-
});
|
|
86
|
-
await action.dispatch({ chatID: "c1" });
|
|
87
|
-
const sentCmd = mockSend.mock.calls[0]![0] as { idempotency_key?: string };
|
|
88
|
-
expect(sentCmd.idempotency_key).toEqual(expect.any(String));
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("per-call callbacks on deduped dispatches", () => {
|
|
93
|
-
it("both callers' onSuccess fire with the same result", async () => {
|
|
94
|
-
let resolveRun: ((v: string) => void) | undefined;
|
|
95
|
-
const action = defineAction<string, string>({
|
|
96
|
-
name: "test.dedupe_cb",
|
|
97
|
-
dedupe: true,
|
|
98
|
-
run: () =>
|
|
99
|
-
new Promise<string>((r) => {
|
|
100
|
-
resolveRun = r;
|
|
101
|
-
}),
|
|
102
|
-
});
|
|
103
|
-
const onSuccess1 = vi.fn();
|
|
104
|
-
const onSuccess2 = vi.fn();
|
|
105
|
-
const p1 = action.dispatch("k", { onSuccess: onSuccess1 });
|
|
106
|
-
const p2 = action.dispatch("k", { onSuccess: onSuccess2 });
|
|
107
|
-
resolveRun!("shared");
|
|
108
|
-
await Promise.all([p1, p2]);
|
|
109
|
-
expect(onSuccess1).toHaveBeenCalledWith("shared", "k");
|
|
110
|
-
expect(onSuccess2).toHaveBeenCalledWith("shared", "k");
|
|
111
|
-
});
|
|
112
|
-
});
|