repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / test
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});