Skip to content

Commit 3e9bf27

Browse files
committed
[form] Add simple test cases for useAction
1 parent 32f45df commit 3e9bf27

6 files changed

+125
-89
lines changed

mkfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ f/:
2424
c:
2525
pnpm run check
2626
t:
27-
pnpm run test
27+
pnpm run test $@
2828
popd
2929

3030
ds/:

packages/form/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@types/json-schema": "^7.0.15",
4747
"@types/json-schema-merge-allof": "^0.6.5",
4848
"deep-freeze-es6": "^3.0.2",
49+
"svelte": "catalog:",
4950
"svelte-check": "catalog:",
5051
"vite": "catalog:",
5152
"vitest": "catalog:"
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { tick } from "svelte";
3+
4+
import {
5+
abortPrevious,
6+
forgetPrevious,
7+
ignoreNewUntilPreviousIsFinished,
8+
Status,
9+
useAction,
10+
} from "./use-action.svelte.js";
11+
12+
describe("useAction", () => {
13+
beforeEach(() => {
14+
vi.useFakeTimers();
15+
});
16+
afterEach(() => {
17+
vi.restoreAllMocks();
18+
});
19+
describe("Status", () => {
20+
it("Should correctly update status during normal flow", async () => {
21+
const action = useAction({
22+
do: () => Promise.resolve(),
23+
});
24+
const promise = action.run();
25+
expect(action.status).toBe(Status.Processed);
26+
await promise;
27+
expect(action.status).toBe(Status.Success);
28+
});
29+
it("Should correctly update statuses: processed -> delayed -> failed", async () => {
30+
const action = useAction({
31+
do: () =>
32+
new Promise((resolve) => {
33+
setTimeout(() => {
34+
resolve(0);
35+
}, 100);
36+
}),
37+
delayedMs: 10,
38+
timeoutMs: 50,
39+
});
40+
const promise = action.run();
41+
vi.advanceTimersByTime(10);
42+
expect(action.status).toBe(Status.Delayed);
43+
vi.advanceTimersByTime(40);
44+
expect(action.status).toBe(Status.Failed);
45+
vi.advanceTimersByTime(50);
46+
await promise
47+
expect(action.status).toBe(Status.Failed);
48+
});
49+
});
50+
describe("Combinator", () => {
51+
it("Should ignore new action until the previous action is completed", async () => {
52+
const impl = vi.fn(
53+
() =>
54+
new Promise((resolve) => {
55+
setTimeout(() => {
56+
resolve(0);
57+
}, 100);
58+
})
59+
);
60+
const action = useAction({
61+
do: impl,
62+
combinator: ignoreNewUntilPreviousIsFinished,
63+
});
64+
action.run();
65+
action.run();
66+
vi.advanceTimersByTime(100);
67+
await tick();
68+
action.run();
69+
expect(impl).toBeCalledTimes(2);
70+
});
71+
it("Should forget previous action with 'forgetPrevious' combinator", async () => {
72+
let count = 0;
73+
const onSuccess = vi.fn();
74+
const action = useAction({
75+
do: () => Promise.resolve(count++),
76+
combinator: forgetPrevious,
77+
onSuccess,
78+
});
79+
action.run();
80+
action.run();
81+
await tick();
82+
expect(onSuccess).toBeCalledTimes(1);
83+
expect(onSuccess).toBeCalledWith(1);
84+
});
85+
it("Should abort previous action with 'abortPrevious' combinator", async () => {
86+
const onAbort = vi.fn();
87+
let count = 0;
88+
const impl = vi.fn((signal: AbortSignal) => {
89+
signal.addEventListener("abort", onAbort);
90+
return Promise.resolve(count++)
91+
})
92+
const onSuccess = vi.fn();
93+
const action = useAction({
94+
do: impl,
95+
combinator: abortPrevious,
96+
onSuccess,
97+
});
98+
action.run();
99+
action.run();
100+
await action.run()
101+
expect(onAbort).toBeCalledTimes(2);
102+
expect(impl).toBeCalledTimes(3);
103+
expect(onSuccess).toBeCalledTimes(1);
104+
expect(onSuccess).toBeCalledWith(2);
105+
});
106+
});
107+
});

packages/form/src/use-action.svelte.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function useAction<T, R, E = unknown>(
9191
options.combinator ?? ignoreNewUntilPreviousIsFinished
9292
);
9393

94-
let state = $state<ActionState<E>>({
94+
let state = $state.raw<ActionState<E>>({
9595
status: Status.IDLE,
9696
});
9797
let abortController: AbortController | null = null;
@@ -110,18 +110,17 @@ export function useAction<T, R, E = unknown>(
110110
clearTimeouts();
111111
}
112112

113-
const status = $derived(state.status);
114-
const isSuccess = $derived(status === Status.Success);
115-
const isFailed = $derived(status === Status.Failed);
116-
const isProcessed = $derived(status === Status.Processed);
117-
const isDelayed = $derived(status === Status.Delayed);
113+
const isSuccess = $derived(state.status === Status.Success);
114+
const isFailed = $derived(state.status === Status.Failed);
115+
const isProcessed = $derived(state.status === Status.Processed);
116+
const isDelayed = $derived(state.status === Status.Delayed);
118117

119118
return {
120119
get state() {
121120
return state;
122121
},
123122
get status() {
124-
return status;
123+
return state.status;
125124
},
126125
get isSuccess() {
127126
return isSuccess;
@@ -144,18 +143,18 @@ export function useAction<T, R, E = unknown>(
144143
abort();
145144
}
146145
state = {
147-
status: status === Status.Delayed ? Status.Processed : Status.IDLE,
146+
status: state.status === Status.Delayed ? Status.Delayed : Status.Processed,
148147
};
149148
abortController = new AbortController();
150149
const action = options.do(abortController.signal, value).then(
151150
(result) => {
152-
if (ref?.deref() !== action) return;
151+
if (ref?.deref() !== action || state.status === Status.Failed) return;
153152
state = { status: Status.Success };
154153
options.onSuccess?.(result);
155154
},
156155
(error) => {
157156
// Action may have been aborted by user or timeout
158-
if (ref?.deref() !== action || status === Status.Failed) return;
157+
if (ref?.deref() !== action || state.status === Status.Failed) return;
159158
state = { status: Status.Failed, reason: "error", error };
160159
options.onFailure?.(state);
161160
}

packages/form/vite.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default defineConfig({
99
test: {},
1010
plugins: [svelte()],
1111
resolve: {
12+
conditions: process.env.VITEST ? ["browser"] : [],
1213
alias: {
1314
"@": resolve(__dirname, "src"),
1415
},

pnpm-lock.yaml

+6-78
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)