Vlad
·
2025-06-15
matcher.test.ts
1import {
2 type ThunkCtx,
3 createAction,
4 createThunks,
5 sleep,
6 takeLatest,
7} from "../index.js";
8import { matcher } from "../matcher.js";
9import { createStore } from "../store/index.js";
10import { expect, test } from "../test.js";
11
12test("true", () => {
13 expect(true).toBe(true);
14});
15
16// The main thing
17test("createAction should not match all actions", async () => {
18 expect.assertions(1);
19
20 const store = createStore({ initialState: {} });
21 const matchedActions: string[] = [];
22
23 const testAction = createAction("test/action");
24
25 function* testFn(action: any) {
26 matchedActions.push(action.type);
27 }
28
29 function* root() {
30 yield* takeLatest(testAction, testFn);
31 }
32 const task = store.run(root);
33
34 store.dispatch({ type: "test/action", payload: { MenuOpened: "test" } });
35 store.dispatch({ type: "store", payload: { something: "else" } });
36 store.dispatch({ type: "other/action", payload: { data: "test" } });
37
38 await new Promise((resolve) => setTimeout(resolve, 100));
39 expect(matchedActions).toEqual(["test/action"]);
40 await task.halt();
41});
42
43test("matcher should correctly identify createAction functions", () => {
44 expect.assertions(2);
45
46 const actionCreator = createAction("test/action");
47 const match = matcher(actionCreator);
48 expect(match({ type: "test/action", payload: {} })).toBe(true);
49 expect(match({ type: "other/action", payload: {} })).toBe(false);
50});
51
52test("typed createAction should work with takeLatest without type casting", async () => {
53 expect.assertions(1);
54
55 const store = createStore({ initialState: {} });
56 const matchedActions: string[] = [];
57
58 //typed action creator - this should work without 'as any'
59 const typedAction = createAction<{ MenuOpened: string }>("TYPED_ACTION");
60
61 function* handler(action: any) {
62 matchedActions.push(action.type);
63 }
64
65 function* root() {
66 // Should compile without TypeScript errors - no 'as any' needed
67 yield* takeLatest(typedAction, handler);
68 }
69
70 const task = store.run(root);
71
72 // dispatch the typed action
73 store.dispatch(typedAction({ MenuOpened: "settings" }));
74
75 // unrelated actions that should NOT trigger handler
76 store.dispatch({ type: "RANDOM_ACTION", payload: { data: "test" } });
77
78 await new Promise((resolve) => setTimeout(resolve, 100));
79 expect(matchedActions).toEqual(["TYPED_ACTION"]);
80
81 await task.halt();
82});
83
84test("should correctly identify starfx thunk as a thunk", async () => {
85 expect.assertions(6);
86
87 const thunks = createThunks();
88 thunks.use(thunks.routes());
89
90 const store = createStore({
91 initialState: {
92 users: {},
93 },
94 });
95 store.run(thunks.bootup);
96
97 const myThunk = thunks.create("my-thunk", function* (_ctx, next) {
98 yield* next();
99 });
100
101 // Test that thunk has the expected properties for isThunk detection
102 expect(typeof myThunk.run).toBe("function");
103 expect(typeof myThunk.use).toBe("function");
104 expect(typeof myThunk.name).toBe("string");
105 expect(typeof myThunk.toString).toBe("function");
106
107 // Verify it does NOT have the _starfx property (that's for action creators)
108 expect((myThunk as any)._starfx).toBeUndefined();
109
110 // Verify it does NOT have a key property directly on the function
111 expect(typeof (myThunk as any).key).toBe("undefined");
112
113 await new Promise((resolve) => setTimeout(resolve, 10));
114});
115
116test("matcher should correctly identify thunk functions", async () => {
117 expect.assertions(3);
118
119 const thunks = createThunks();
120 thunks.use(thunks.routes());
121
122 const store = createStore({
123 initialState: {
124 users: {},
125 },
126 });
127 store.run(thunks.bootup);
128
129 const myThunk = thunks.create("my-thunk", function* (_ctx, next) {
130 yield* next();
131 });
132
133 // Test that the matcher correctly identifies the thunk
134 const match = matcher(myThunk);
135 expect(match({ type: "my-thunk", payload: {} })).toBe(true);
136 expect(match({ type: "other-action", payload: {} })).toBe(false);
137
138 // Verify the thunk's toString method returns the correct type
139 expect(myThunk.toString()).toBe("my-thunk");
140
141 await new Promise((resolve) => setTimeout(resolve, 10));
142});
143
144// the bug that determined we needed to write this matcher
145test("some bug: createAction incorrectly matching all actions", async () => {
146 expect.assertions(1);
147
148 const store = createStore({ initialState: {} });
149 const matchedActions: string[] = [];
150
151 const testAction = createAction<{ MenuOpened: any }>("ACTION");
152
153 // Create a saga that should only respond to this specific action
154 function* testFn(action: any) {
155 matchedActions.push(action.type);
156 yield* sleep(1);
157 }
158
159 function* root() {
160 yield* takeLatest(testAction, testFn);
161 }
162
163 const task = store.run(root);
164
165 store.dispatch(testAction({ MenuOpened: "first" }));
166 store.dispatch({ type: "store", payload: { something: "else" } });
167 store.dispatch({ type: "other/action", payload: { data: "test" } });
168
169 await new Promise((resolve) => setTimeout(resolve, 100));
170
171 expect(matchedActions).toEqual(["ACTION"]);
172
173 await task.halt();
174});
175
176test("should show the difference between createAction and thunk properties", () => {
177 expect.assertions(8);
178
179 // starfx createAction
180 const actionCreator = createAction<{ test: string }>("test/action");
181
182 // starfx thunk
183 const thunks = createThunks();
184 const myThunk = thunks.create("test-thunk", function* (_ctx: ThunkCtx, next) {
185 yield* next();
186 });
187
188 // Check properties of createAction
189 expect(typeof actionCreator).toBe("function");
190 expect(typeof actionCreator.toString).toBe("function");
191 expect(actionCreator.toString()).toBe("test/action");
192 expect(typeof (actionCreator as any).run).toBe("undefined"); // createAction doesn't have run
193
194 // Check properties of thunk
195 expect(typeof myThunk).toBe("function");
196 expect(typeof myThunk.toString).toBe("function");
197 expect(typeof myThunk.run).toBe("function"); // thunk has run
198 expect(typeof myThunk.name).toBe("string"); // actionFn
199});
200
201test("debug: what path does createAction take in matcher", async () => {
202 expect.assertions(7);
203
204 const actionCreator = createAction<{ test: string }>("test/action");
205
206 // Check that createAction has the _starfx branding property
207 expect((actionCreator as any)._starfx).toBe(true);
208
209 // Verify it's NOT identified as a thunk (should fail isThunk checks)
210 const hasRun = typeof (actionCreator as any).run === "function";
211 const hasUse = typeof (actionCreator as any).use === "function";
212 const hasKey = typeof (actionCreator as any).key === "string";
213 expect(hasRun).toBe(false);
214 expect(hasUse).toBe(false);
215 expect(hasKey).toBe(false);
216
217 // Verify it has the properties needed for isActionCreator path
218 const hasToString = typeof actionCreator.toString === "function";
219 const isFunction = typeof actionCreator === "function";
220 expect(hasToString).toBe(true);
221 expect(isFunction).toBe(true);
222
223 // Most importantly: verify the matcher correctly identifies it as an action creator
224 const match = matcher(actionCreator);
225 expect(match({ type: "test/action", payload: { test: "value" } })).toBe(true);
226
227 await new Promise((resolve) => setTimeout(resolve, 10));
228});