Skip to content

Commit 8074e91

Browse files
committed
add tests for data functions, co-locate new specs
1 parent 30f0866 commit 8074e91

File tree

14 files changed

+2017
-24
lines changed

14 files changed

+2017
-24
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ jobs:
1717
- name: Use Node.js from nvmrc
1818
uses: actions/setup-node@v4
1919
with:
20-
node-version-file: '.nvmrc'
21-
registry-url: 'https://registry.npmjs.org'
20+
node-version-file: ".nvmrc"
21+
registry-url: "https://registry.npmjs.org"
2222

2323
- name: Install pnpm
2424
uses: pnpm/action-setup@v4
2525
with:
2626
version: 9
2727

2828
- name: Install dependencies
29-
run: pnpm install
29+
run: pnpm i --frozen-lockfile
3030

3131
- name: Run tests
3232
run: pnpm run test

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default {
1818
extensions: [".js", ".ts", ".tsx"],
1919
babelHelpers: "bundled",
2020
presets: ["solid", "@babel/preset-typescript"],
21-
exclude: "node_modules/**"
21+
exclude: ["node_modules/**", "**/*.spec.ts"]
2222
})
2323
]
2424
};

src/data/action.spec.ts

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import { createRoot } from "solid-js";
2+
import { vi } from "vitest";
3+
import { action, useAction, useSubmission, useSubmissions, actions } from "./action.js";
4+
import type { RouterContext } from "../types.js";
5+
import { createMockRouter } from "../../test/helpers.js";
6+
7+
vi.mock("../src/utils.js", () => ({
8+
mockBase: "https://action"
9+
}));
10+
11+
let mockRouterContext: RouterContext;
12+
13+
vi.mock("../routing.js", () => ({
14+
useRouter: () => mockRouterContext,
15+
createRouterContext: () => createMockRouter(),
16+
RouterContextObj: {},
17+
RouteContextObj: {},
18+
useRoute: () => mockRouterContext.base,
19+
useResolvedPath: () => "/",
20+
useHref: () => "/",
21+
useNavigate: () => vi.fn(),
22+
useLocation: () => mockRouterContext.location,
23+
useRouteData: () => undefined,
24+
useMatch: () => null,
25+
useParams: () => ({}),
26+
useSearchParams: () => [{}, vi.fn()],
27+
useIsRouting: () => false,
28+
usePreloadRoute: () => vi.fn(),
29+
useBeforeLeave: () => vi.fn()
30+
}));
31+
32+
describe("action", () => {
33+
beforeEach(() => {
34+
actions.clear();
35+
mockRouterContext = createMockRouter();
36+
});
37+
38+
test("should create an action function with `url` property", () => {
39+
const testAction = action(async (data: string) => {
40+
return `processed: ${data}`;
41+
}, "test-action");
42+
43+
expect(typeof testAction).toBe("function");
44+
expect(testAction.url).toBe("https://action/test-action");
45+
});
46+
47+
test.skip("should create action with auto-generated hash when no `name` provided", () => {
48+
const testFn = async (data: string) => `result: ${data}`;
49+
const testAction = action(testFn);
50+
51+
expect(testAction.url).toMatch(/^https:\/\/action\/-?\d+$/);
52+
expect((testAction as any).name).toMatch(/^-?\d+$/);
53+
});
54+
55+
test.skip("should use it as `name` when `options` are provided as a string", () => {
56+
const testFn = async (data: string) => `result: ${data}`;
57+
const testAction = action(testFn, "test-action");
58+
59+
expect(testAction.url).toMatch("https://action/test-action");
60+
expect((testAction as any).name).toBe("test-action");
61+
});
62+
63+
test.skip("should use `name` when provided in object options", () => {
64+
const testFn = async (data: string) => `result: ${data}`;
65+
const testAction = action(testFn, { name: "test-action" });
66+
67+
expect(testAction.url).toMatch("https://action/test-action");
68+
expect((testAction as any).name).toBe("test-action");
69+
});
70+
71+
test("should register action in actions map", () => {
72+
const testAction = action(async () => "result", "register-test");
73+
74+
expect(actions.has(testAction.url)).toBe(true);
75+
expect(actions.get(testAction.url)).toBe(testAction);
76+
});
77+
78+
test("should support `.with` method for currying arguments", () => {
79+
const baseAction = action(async (prefix: string, data: string) => {
80+
return `${prefix}: ${data}`;
81+
}, "with-test");
82+
83+
const curriedAction = baseAction.with("PREFIX");
84+
85+
expect(typeof curriedAction).toBe("function");
86+
expect(curriedAction.url).toMatch(/with-test\?args=/);
87+
});
88+
89+
test("should execute action and create submission", async () => {
90+
return createRoot(async () => {
91+
const testAction = action(async (data: string) => {
92+
return `processed: ${data}`;
93+
}, "execute-test");
94+
95+
const boundAction = useAction(testAction);
96+
const promise = boundAction("test-data");
97+
98+
const submissions = mockRouterContext.submissions[0]();
99+
expect(submissions).toHaveLength(1);
100+
expect(submissions[0].input).toEqual(["test-data"]);
101+
expect(submissions[0].pending).toBe(true);
102+
103+
const result = await promise;
104+
expect(result).toBe("processed: test-data");
105+
});
106+
});
107+
108+
test("should handle action errors", async () => {
109+
return createRoot(async () => {
110+
const errorAction = action(async () => {
111+
throw new Error("Test error");
112+
}, "error-test");
113+
114+
const boundAction = useAction(errorAction);
115+
116+
try {
117+
await boundAction();
118+
} catch (error) {
119+
expect((error as Error).message).toBe("Test error");
120+
}
121+
122+
const submissions = mockRouterContext.submissions[0]();
123+
expect(submissions[0].error.message).toBe("Test error");
124+
});
125+
});
126+
127+
test("should support `onComplete` callback", async () => {
128+
return createRoot(async () => {
129+
const onComplete = vi.fn();
130+
const testAction = action(async (data: string) => `result: ${data}`, {
131+
name: "callback-test",
132+
onComplete
133+
});
134+
135+
const boundAction = useAction(testAction);
136+
await boundAction("test");
137+
138+
expect(onComplete).toHaveBeenCalledWith(
139+
expect.objectContaining({
140+
result: "result: test",
141+
error: undefined,
142+
pending: false
143+
})
144+
);
145+
});
146+
});
147+
});
148+
149+
describe("useSubmissions", () => {
150+
beforeEach(() => {
151+
mockRouterContext = createMockRouter();
152+
});
153+
154+
test("should return submissions for specific action", () => {
155+
return createRoot(() => {
156+
const testAction = action(async () => "result", "submissions-test");
157+
158+
mockRouterContext.submissions[1](submissions => [
159+
...submissions,
160+
{
161+
input: ["data1"],
162+
url: testAction.url,
163+
result: "result1",
164+
error: undefined,
165+
pending: false,
166+
clear: vi.fn(),
167+
retry: vi.fn()
168+
},
169+
{
170+
input: ["data2"],
171+
url: testAction.url,
172+
result: undefined,
173+
error: undefined,
174+
pending: true,
175+
clear: vi.fn(),
176+
retry: vi.fn()
177+
}
178+
]);
179+
180+
const submissions = useSubmissions(testAction);
181+
182+
expect(submissions).toHaveLength(2);
183+
expect(submissions[0].input).toEqual(["data1"]);
184+
expect(submissions[1].input).toEqual(["data2"]);
185+
expect(submissions.pending).toBe(true);
186+
});
187+
});
188+
189+
test("should filter submissions when filter function provided", () => {
190+
return createRoot(() => {
191+
const testAction = action(async (data: string) => data, "filter-test");
192+
193+
mockRouterContext.submissions[1](submissions => [
194+
...submissions,
195+
{
196+
input: ["keep"],
197+
url: testAction.url,
198+
result: "result1",
199+
error: undefined,
200+
pending: false,
201+
clear: vi.fn(),
202+
retry: vi.fn()
203+
},
204+
{
205+
input: ["skip"],
206+
url: testAction.url,
207+
result: "result2",
208+
error: undefined,
209+
pending: false,
210+
clear: vi.fn(),
211+
retry: vi.fn()
212+
}
213+
]);
214+
215+
const submissions = useSubmissions(testAction, input => input[0] === "keep");
216+
217+
expect(submissions).toHaveLength(1);
218+
expect(submissions[0].input).toEqual(["keep"]);
219+
});
220+
});
221+
222+
test("should return pending false when no pending submissions", () => {
223+
return createRoot(() => {
224+
const testAction = action(async () => "result", "no-pending-test");
225+
226+
mockRouterContext.submissions[1](submissions => [
227+
...submissions,
228+
{
229+
input: ["data"],
230+
url: testAction.url,
231+
result: "result",
232+
error: undefined,
233+
pending: false,
234+
clear: vi.fn(),
235+
retry: vi.fn()
236+
}
237+
]);
238+
239+
const submissions = useSubmissions(testAction);
240+
expect(submissions.pending).toBe(false);
241+
});
242+
});
243+
});
244+
245+
describe("useSubmission", () => {
246+
beforeEach(() => {
247+
mockRouterContext = createMockRouter();
248+
});
249+
250+
test("should return latest submission for action", () => {
251+
return createRoot(() => {
252+
const testAction = action(async () => "result", "latest-test");
253+
254+
mockRouterContext.submissions[1](submissions => [
255+
...submissions,
256+
{
257+
input: ["data1"],
258+
url: testAction.url,
259+
result: "result1",
260+
error: undefined,
261+
pending: false,
262+
clear: vi.fn(),
263+
retry: vi.fn()
264+
},
265+
{
266+
input: ["data2"],
267+
url: testAction.url,
268+
result: "result2",
269+
error: undefined,
270+
pending: false,
271+
clear: vi.fn(),
272+
retry: vi.fn()
273+
}
274+
]);
275+
276+
const submission = useSubmission(testAction);
277+
278+
expect(submission.input).toEqual(["data2"]);
279+
expect(submission.result).toBe("result2");
280+
});
281+
});
282+
283+
test("should return stub when no submissions exist", () => {
284+
return createRoot(() => {
285+
const testAction = action(async () => "result", "stub-test");
286+
const submission = useSubmission(testAction);
287+
288+
expect(submission.clear).toBeDefined();
289+
expect(submission.retry).toBeDefined();
290+
expect(typeof submission.clear).toBe("function");
291+
expect(typeof submission.retry).toBe("function");
292+
});
293+
});
294+
295+
test("should filter submissions when filter function provided", () => {
296+
return createRoot(() => {
297+
const testAction = action(async (data: string) => data, "filter-submission-test");
298+
299+
mockRouterContext.submissions[1](submissions => [
300+
...submissions,
301+
{
302+
input: ["skip"],
303+
url: testAction.url,
304+
result: "result1",
305+
error: undefined,
306+
pending: false,
307+
clear: vi.fn(),
308+
retry: vi.fn()
309+
},
310+
{
311+
input: ["keep"],
312+
url: testAction.url,
313+
result: "result2",
314+
error: undefined,
315+
pending: false,
316+
clear: vi.fn(),
317+
retry: vi.fn()
318+
}
319+
]);
320+
321+
const submission = useSubmission(testAction, input => input[0] === "keep");
322+
323+
expect(submission.input).toEqual(["keep"]);
324+
expect(submission.result).toBe("result2");
325+
});
326+
});
327+
});
328+
329+
describe("useAction", () => {
330+
beforeEach(() => {
331+
mockRouterContext = createMockRouter();
332+
});
333+
334+
test("should return bound action function", () => {
335+
return createRoot(() => {
336+
const testAction = action(async (data: string) => `result: ${data}`, "bound-test");
337+
const boundAction = useAction(testAction);
338+
339+
expect(typeof boundAction).toBe("function");
340+
});
341+
});
342+
343+
test("should execute action through useAction", async () => {
344+
return createRoot(async () => {
345+
const testAction = action(async (data: string) => {
346+
await new Promise(resolve => setTimeout(resolve, 1));
347+
return `result: ${data}`;
348+
}, "context-test");
349+
350+
const boundAction = useAction(testAction);
351+
const result = await boundAction("test-data");
352+
353+
expect(result).toBe("result: test-data");
354+
});
355+
});
356+
});

0 commit comments

Comments
 (0)