@cplieger/actions 1.0.1 → 1.0.2
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 +4 -1
- 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/loading.test.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
-
import { resetActionFramework } from "./__test-helpers__/action-test-setup.js";
|
|
4
|
-
vi.mock("./notifier.js", () => ({
|
|
5
|
-
configure: vi.fn(),
|
|
6
|
-
notifySuccess: vi.fn(),
|
|
7
|
-
notifyError: vi.fn(),
|
|
8
|
-
_resetNotifierForTest: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
import { defineAction } from "./define.js";
|
|
11
|
-
import { bindLoadingState } from "./loading.js";
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
resetActionFramework();
|
|
15
|
-
vi.clearAllMocks();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("bindLoadingState — single name", () => {
|
|
19
|
-
it("toggles disabled while action is pending", async () => {
|
|
20
|
-
let resolveRun: (value: string) => void;
|
|
21
|
-
const action = defineAction({
|
|
22
|
-
name: "test.bind1",
|
|
23
|
-
run: () =>
|
|
24
|
-
new Promise<string>((r) => {
|
|
25
|
-
resolveRun = r;
|
|
26
|
-
}),
|
|
27
|
-
});
|
|
28
|
-
const btn = document.createElement("button");
|
|
29
|
-
bindLoadingState("test.bind1", btn);
|
|
30
|
-
expect(btn.disabled).toBe(false);
|
|
31
|
-
const p = action.dispatch({});
|
|
32
|
-
expect(btn.disabled).toBe(true);
|
|
33
|
-
resolveRun!("ok");
|
|
34
|
-
await p;
|
|
35
|
-
expect(btn.disabled).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("sets aria-busy by default + clears on completion", async () => {
|
|
39
|
-
let resolveRun: () => void;
|
|
40
|
-
const action = defineAction({
|
|
41
|
-
name: "test.bind2",
|
|
42
|
-
run: () =>
|
|
43
|
-
new Promise<void>((r) => {
|
|
44
|
-
resolveRun = r;
|
|
45
|
-
}),
|
|
46
|
-
});
|
|
47
|
-
const btn = document.createElement("button");
|
|
48
|
-
bindLoadingState("test.bind2", btn);
|
|
49
|
-
const p = action.dispatch({});
|
|
50
|
-
expect(btn.getAttribute("aria-busy")).toBe("true");
|
|
51
|
-
resolveRun!();
|
|
52
|
-
await p;
|
|
53
|
-
expect(btn.getAttribute("aria-busy")).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("pendingClass adds + removes a CSS class", async () => {
|
|
57
|
-
let resolveRun: () => void;
|
|
58
|
-
const action = defineAction({
|
|
59
|
-
name: "test.bind4",
|
|
60
|
-
run: () =>
|
|
61
|
-
new Promise<void>((r) => {
|
|
62
|
-
resolveRun = r;
|
|
63
|
-
}),
|
|
64
|
-
});
|
|
65
|
-
const btn = document.createElement("button");
|
|
66
|
-
bindLoadingState("test.bind4", btn, { pendingClass: "btn-loading" });
|
|
67
|
-
const p = action.dispatch({});
|
|
68
|
-
expect(btn.classList.contains("btn-loading")).toBe(true);
|
|
69
|
-
resolveRun!();
|
|
70
|
-
await p;
|
|
71
|
-
expect(btn.classList.contains("btn-loading")).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("multiple in-flight instances keep btn disabled until ALL complete", async () => {
|
|
75
|
-
const resolvers: (() => void)[] = [];
|
|
76
|
-
const action = defineAction({
|
|
77
|
-
name: "test.bind7",
|
|
78
|
-
run: () =>
|
|
79
|
-
new Promise<void>((r) => {
|
|
80
|
-
resolvers.push(r);
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
|
-
const btn = document.createElement("button");
|
|
84
|
-
bindLoadingState("test.bind7", btn);
|
|
85
|
-
const p1 = action.dispatch({});
|
|
86
|
-
const p2 = action.dispatch({});
|
|
87
|
-
expect(btn.disabled).toBe(true);
|
|
88
|
-
resolvers[0]!();
|
|
89
|
-
await p1;
|
|
90
|
-
expect(btn.disabled).toBe(true);
|
|
91
|
-
resolvers[1]!();
|
|
92
|
-
await p2;
|
|
93
|
-
expect(btn.disabled).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns an unsubscribe that stops further updates", async () => {
|
|
97
|
-
let resolveRun: () => void;
|
|
98
|
-
const action = defineAction({
|
|
99
|
-
name: "test.bind8",
|
|
100
|
-
run: () =>
|
|
101
|
-
new Promise<void>((r) => {
|
|
102
|
-
resolveRun = r;
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
const btn = document.createElement("button");
|
|
106
|
-
const unbind = bindLoadingState("test.bind8", btn);
|
|
107
|
-
unbind();
|
|
108
|
-
const p = action.dispatch({});
|
|
109
|
-
expect(btn.disabled).toBe(false);
|
|
110
|
-
resolveRun!();
|
|
111
|
-
await p;
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("bindLoadingState — multi-name", () => {
|
|
116
|
-
it("disables while ANY named action is pending", async () => {
|
|
117
|
-
let resolve1!: () => void;
|
|
118
|
-
let resolve2!: () => void;
|
|
119
|
-
const a1 = defineAction({
|
|
120
|
-
name: "test.multi1",
|
|
121
|
-
run: () =>
|
|
122
|
-
new Promise<void>((r) => {
|
|
123
|
-
resolve1 = r;
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
const a2 = defineAction({
|
|
127
|
-
name: "test.multi2",
|
|
128
|
-
run: () =>
|
|
129
|
-
new Promise<void>((r) => {
|
|
130
|
-
resolve2 = r;
|
|
131
|
-
}),
|
|
132
|
-
});
|
|
133
|
-
const btn = document.createElement("button");
|
|
134
|
-
bindLoadingState(["test.multi1", "test.multi2"], btn);
|
|
135
|
-
expect(btn.disabled).toBe(false);
|
|
136
|
-
const p1 = a1.dispatch({});
|
|
137
|
-
expect(btn.disabled).toBe(true);
|
|
138
|
-
const p2 = a2.dispatch({});
|
|
139
|
-
resolve1();
|
|
140
|
-
await p1;
|
|
141
|
-
expect(btn.disabled).toBe(true);
|
|
142
|
-
resolve2();
|
|
143
|
-
await p2;
|
|
144
|
-
expect(btn.disabled).toBe(false);
|
|
145
|
-
});
|
|
146
|
-
});
|
package/src/new-features.test.ts
DELETED
|
@@ -1,246 +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, subscribeByName, getActionLog } from "./registry.js";
|
|
11
|
-
import { _resetForTest as resetCleanup } from "./cleanup.js";
|
|
12
|
-
import { ActionError } from "./error.js";
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
resetDefine();
|
|
16
|
-
resetRegistry();
|
|
17
|
-
resetCleanup();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// 1. Definition-level onSuccess / onError / onSettled (TanStack pattern)
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
describe("definition-level callbacks (TanStack pattern)", () => {
|
|
24
|
-
it("onSuccess fires on every successful dispatch", async () => {
|
|
25
|
-
const defOnSuccess = vi.fn();
|
|
26
|
-
const action = defineAction({
|
|
27
|
-
name: "def.ok",
|
|
28
|
-
run: async (n: number) => n * 3,
|
|
29
|
-
onSuccess: defOnSuccess,
|
|
30
|
-
});
|
|
31
|
-
await action.dispatch(2);
|
|
32
|
-
await action.dispatch(4);
|
|
33
|
-
expect(defOnSuccess).toHaveBeenCalledTimes(2);
|
|
34
|
-
expect(defOnSuccess).toHaveBeenCalledWith(6, 2);
|
|
35
|
-
expect(defOnSuccess).toHaveBeenCalledWith(12, 4);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("onError fires on every failed dispatch", async () => {
|
|
39
|
-
const defOnError = vi.fn();
|
|
40
|
-
const action = defineAction({
|
|
41
|
-
name: "def.err",
|
|
42
|
-
run: async () => {
|
|
43
|
-
throw new ActionError("boom");
|
|
44
|
-
},
|
|
45
|
-
onError: defOnError,
|
|
46
|
-
});
|
|
47
|
-
await action.dispatch("x");
|
|
48
|
-
expect(defOnError).toHaveBeenCalledWith(expect.objectContaining({ message: "boom" }), "x");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("onError does NOT fire on cancellation", async () => {
|
|
52
|
-
const defOnError = vi.fn();
|
|
53
|
-
const action = defineAction({
|
|
54
|
-
name: "def.cancel-no-err",
|
|
55
|
-
run: (_a, signal) =>
|
|
56
|
-
new Promise<void>((_, rej) => {
|
|
57
|
-
signal.addEventListener("abort", () => rej(new Error("aborted")));
|
|
58
|
-
}),
|
|
59
|
-
onError: defOnError,
|
|
60
|
-
});
|
|
61
|
-
const p = action.dispatch("a");
|
|
62
|
-
action.cancel();
|
|
63
|
-
await p;
|
|
64
|
-
expect(defOnError).not.toHaveBeenCalled();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("onSettled fires on success, error, and cancellation", async () => {
|
|
68
|
-
const defOnSettled = vi.fn();
|
|
69
|
-
const okAction = defineAction({
|
|
70
|
-
name: "def.settled.ok",
|
|
71
|
-
run: async () => "r",
|
|
72
|
-
onSettled: defOnSettled,
|
|
73
|
-
});
|
|
74
|
-
await okAction.dispatch("s");
|
|
75
|
-
expect(defOnSettled).toHaveBeenCalledWith("s");
|
|
76
|
-
|
|
77
|
-
defOnSettled.mockClear();
|
|
78
|
-
const errAction = defineAction({
|
|
79
|
-
name: "def.settled.err",
|
|
80
|
-
run: async () => {
|
|
81
|
-
throw new ActionError("e");
|
|
82
|
-
},
|
|
83
|
-
onSettled: defOnSettled,
|
|
84
|
-
});
|
|
85
|
-
await errAction.dispatch("e");
|
|
86
|
-
expect(defOnSettled).toHaveBeenCalledWith("e");
|
|
87
|
-
|
|
88
|
-
defOnSettled.mockClear();
|
|
89
|
-
const cancelAction = defineAction({
|
|
90
|
-
name: "def.settled.cancel",
|
|
91
|
-
run: (_a, signal) =>
|
|
92
|
-
new Promise<void>((_, rej) => {
|
|
93
|
-
signal.addEventListener("abort", () => rej(new Error("aborted")));
|
|
94
|
-
}),
|
|
95
|
-
onSettled: defOnSettled,
|
|
96
|
-
});
|
|
97
|
-
const p = cancelAction.dispatch("c");
|
|
98
|
-
cancelAction.cancel();
|
|
99
|
-
await p;
|
|
100
|
-
expect(defOnSettled).toHaveBeenCalledWith("c");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("definition-level fires BEFORE per-dispatch callbacks", async () => {
|
|
104
|
-
const order: string[] = [];
|
|
105
|
-
const action = defineAction({
|
|
106
|
-
name: "def.order",
|
|
107
|
-
run: async () => "ok",
|
|
108
|
-
onSuccess: () => {
|
|
109
|
-
order.push("def");
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
await action.dispatch("a", {
|
|
113
|
-
onSuccess: () => {
|
|
114
|
-
order.push("dispatch");
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
expect(order).toEqual(["def", "dispatch"]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("throwing in definition-level callback is caught", async () => {
|
|
121
|
-
const consoleErr = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
122
|
-
const action = defineAction({
|
|
123
|
-
name: "def.throw",
|
|
124
|
-
run: async () => "ok",
|
|
125
|
-
onSuccess: () => {
|
|
126
|
-
throw new Error("def boom");
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
const result = await action.dispatch("a");
|
|
130
|
-
expect(result).toBe("ok");
|
|
131
|
-
expect(consoleErr).toHaveBeenCalled();
|
|
132
|
-
consoleErr.mockRestore();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// 2. Per-dispatch abort handle (RTK pattern)
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
describe("per-dispatch abort handle (RTK pattern)", () => {
|
|
140
|
-
it("dispatch() returns a DispatchHandle with abort()", () => {
|
|
141
|
-
const action = defineAction({ name: "handle.shape", run: async () => "ok" });
|
|
142
|
-
const handle = action.dispatch("a");
|
|
143
|
-
expect(handle).toBeInstanceOf(Promise);
|
|
144
|
-
expect(typeof handle.abort).toBe("function");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("abort() cancels only the specific dispatch", async () => {
|
|
148
|
-
const resolvers: ((v: string) => void)[] = [];
|
|
149
|
-
const action = defineAction({
|
|
150
|
-
name: "handle.specific",
|
|
151
|
-
run: (_args, signal) =>
|
|
152
|
-
new Promise<string>((resolve, reject) => {
|
|
153
|
-
resolvers.push(resolve);
|
|
154
|
-
signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
155
|
-
}),
|
|
156
|
-
});
|
|
157
|
-
const h1 = action.dispatch("a");
|
|
158
|
-
const h2 = action.dispatch("b");
|
|
159
|
-
// Wait for both run() calls to register
|
|
160
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
161
|
-
h1.abort();
|
|
162
|
-
resolvers[1]!("two");
|
|
163
|
-
const [r1, r2] = await Promise.all([h1, h2]);
|
|
164
|
-
expect(r1).toBeNull(); // aborted
|
|
165
|
-
expect(r2).toBe("two"); // unaffected
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("abort() after completion is a no-op", async () => {
|
|
169
|
-
const action = defineAction({ name: "handle.noop", run: async () => "done" });
|
|
170
|
-
const handle = action.dispatch("a");
|
|
171
|
-
const result = await handle;
|
|
172
|
-
handle.abort(); // should not throw
|
|
173
|
-
expect(result).toBe("done");
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// 3. timeout option on ActionDefinition
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
describe("timeout option on ActionDefinition", () => {
|
|
181
|
-
it("aborts run() after timeout ms", async () => {
|
|
182
|
-
const action = defineAction({
|
|
183
|
-
name: "timeout.expire",
|
|
184
|
-
timeout: 50,
|
|
185
|
-
run: (_args, signal) =>
|
|
186
|
-
new Promise<string>((resolve, reject) => {
|
|
187
|
-
const t = setTimeout(() => resolve("late"), 200);
|
|
188
|
-
signal.addEventListener("abort", () => {
|
|
189
|
-
clearTimeout(t);
|
|
190
|
-
reject(signal.reason);
|
|
191
|
-
});
|
|
192
|
-
}),
|
|
193
|
-
});
|
|
194
|
-
const result = await action.dispatch("x");
|
|
195
|
-
expect(result).toBeNull(); // timed out
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("does not abort if run() completes within timeout", async () => {
|
|
199
|
-
const action = defineAction({
|
|
200
|
-
name: "timeout.ok",
|
|
201
|
-
timeout: 500,
|
|
202
|
-
run: async () => "fast",
|
|
203
|
-
});
|
|
204
|
-
const result = await action.dispatch("x");
|
|
205
|
-
expect(result).toBe("fast");
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
// 4. subscribeByName + getActionLog public exports
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
describe("subscribeByName (public API)", () => {
|
|
213
|
-
it("receives events only for the named action", async () => {
|
|
214
|
-
const events: string[] = [];
|
|
215
|
-
subscribeByName("sub.target", (inst) => {
|
|
216
|
-
events.push(inst.status);
|
|
217
|
-
});
|
|
218
|
-
const target = defineAction({ name: "sub.target", run: async () => "ok" });
|
|
219
|
-
const other = defineAction({ name: "sub.other", run: async () => "ok" });
|
|
220
|
-
await target.dispatch("a");
|
|
221
|
-
await other.dispatch("b");
|
|
222
|
-
expect(events).toEqual(["pending", "success"]);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("unsubscribe stops events", async () => {
|
|
226
|
-
const events: string[] = [];
|
|
227
|
-
const unsub = subscribeByName("sub.unsub", (inst) => {
|
|
228
|
-
events.push(inst.status);
|
|
229
|
-
});
|
|
230
|
-
const action = defineAction({ name: "sub.unsub", run: async () => "ok" });
|
|
231
|
-
await action.dispatch("a");
|
|
232
|
-
unsub();
|
|
233
|
-
await action.dispatch("b");
|
|
234
|
-
expect(events).toEqual(["pending", "success"]);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("getActionLog (public API)", () => {
|
|
239
|
-
it("returns recent action instances", async () => {
|
|
240
|
-
const action = defineAction({ name: "log.test", run: async () => "r" });
|
|
241
|
-
await action.dispatch("a");
|
|
242
|
-
const log = getActionLog();
|
|
243
|
-
expect(log.length).toBeGreaterThanOrEqual(1);
|
|
244
|
-
expect(log.some((e) => e.name === "log.test" && e.status === "success")).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
});
|