actions.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { describe, it, expect, vi } from "vitest";
  2. import {
  3. resolveAction,
  4. executeAction,
  5. interpolateString,
  6. actionBinding,
  7. } from "./actions";
  8. describe("interpolateString", () => {
  9. it("interpolates ${path} expressions", () => {
  10. const data = { user: { name: "Alice" }, count: 5 };
  11. expect(interpolateString("Hello ${/user/name}!", data)).toBe(
  12. "Hello Alice!",
  13. );
  14. expect(interpolateString("${/user/name} has ${/count} items", data)).toBe(
  15. "Alice has 5 items",
  16. );
  17. });
  18. it("returns string unchanged when no variables", () => {
  19. expect(interpolateString("No vars here", {})).toBe("No vars here");
  20. });
  21. it("replaces missing values with empty string", () => {
  22. expect(interpolateString("Hello ${/missing}!", {})).toBe("Hello !");
  23. });
  24. it("handles multiple occurrences of same variable", () => {
  25. const data = { name: "Bob" };
  26. expect(interpolateString("${/name} says ${/name}", data)).toBe(
  27. "Bob says Bob",
  28. );
  29. });
  30. });
  31. describe("resolveAction", () => {
  32. it("resolves literal params", () => {
  33. const resolved = resolveAction(
  34. {
  35. action: "navigate",
  36. params: { url: "/home", count: 5 },
  37. },
  38. {},
  39. );
  40. expect(resolved.action).toBe("navigate");
  41. expect(resolved.params.url).toBe("/home");
  42. expect(resolved.params.count).toBe(5);
  43. });
  44. it("resolves dynamic $state params", () => {
  45. const data = { userId: 123, settings: { theme: "dark" } };
  46. const resolved = resolveAction(
  47. {
  48. action: "updateUser",
  49. params: {
  50. id: { $state: "/userId" },
  51. theme: { $state: "/settings/theme" },
  52. },
  53. },
  54. data,
  55. );
  56. expect(resolved.params.id).toBe(123);
  57. expect(resolved.params.theme).toBe("dark");
  58. });
  59. it("interpolates confirmation messages", () => {
  60. const data = { user: { name: "Alice" } };
  61. const resolved = resolveAction(
  62. {
  63. action: "delete",
  64. confirm: {
  65. title: "Delete ${/user/name}",
  66. message: "Are you sure you want to delete ${/user/name}?",
  67. },
  68. },
  69. data,
  70. );
  71. expect(resolved.confirm?.title).toBe("Delete Alice");
  72. expect(resolved.confirm?.message).toBe(
  73. "Are you sure you want to delete Alice?",
  74. );
  75. });
  76. it("preserves onSuccess and onError handlers", () => {
  77. const resolved = resolveAction(
  78. {
  79. action: "save",
  80. onSuccess: { navigate: "/success" },
  81. onError: { set: { error: "$error.message" } },
  82. },
  83. {},
  84. );
  85. expect(resolved.onSuccess).toEqual({ navigate: "/success" });
  86. expect(resolved.onError).toEqual({ set: { error: "$error.message" } });
  87. });
  88. });
  89. describe("executeAction", () => {
  90. it("calls the handler with resolved params", async () => {
  91. const handler = vi.fn().mockResolvedValue(undefined);
  92. await executeAction({
  93. action: { action: "test", params: { value: 42 } },
  94. handler,
  95. setState: vi.fn(),
  96. });
  97. expect(handler).toHaveBeenCalledWith({ value: 42 });
  98. });
  99. it("handles onSuccess with navigate", async () => {
  100. const navigate = vi.fn();
  101. await executeAction({
  102. action: {
  103. action: "test",
  104. params: {},
  105. onSuccess: { navigate: "/success" },
  106. },
  107. handler: vi.fn().mockResolvedValue(undefined),
  108. setState: vi.fn(),
  109. navigate,
  110. });
  111. expect(navigate).toHaveBeenCalledWith("/success");
  112. });
  113. it("handles onSuccess with set", async () => {
  114. const setState = vi.fn();
  115. await executeAction({
  116. action: {
  117. action: "test",
  118. params: {},
  119. onSuccess: { set: { saved: true, message: "Done" } },
  120. },
  121. handler: vi.fn().mockResolvedValue(undefined),
  122. setState,
  123. });
  124. expect(setState).toHaveBeenCalledWith("saved", true);
  125. expect(setState).toHaveBeenCalledWith("message", "Done");
  126. });
  127. it("handles onSuccess with action", async () => {
  128. const executeActionFn = vi.fn();
  129. await executeAction({
  130. action: {
  131. action: "test",
  132. params: {},
  133. onSuccess: { action: "followUp" },
  134. },
  135. handler: vi.fn().mockResolvedValue(undefined),
  136. setState: vi.fn(),
  137. executeAction: executeActionFn,
  138. });
  139. expect(executeActionFn).toHaveBeenCalledWith("followUp");
  140. });
  141. it("handles onError with set", async () => {
  142. const setState = vi.fn();
  143. const error = new Error("Something went wrong");
  144. await executeAction({
  145. action: {
  146. action: "test",
  147. params: {},
  148. onError: { set: { error: "$error.message" } },
  149. },
  150. handler: vi.fn().mockRejectedValue(error),
  151. setState,
  152. });
  153. expect(setState).toHaveBeenCalledWith("error", "Something went wrong");
  154. });
  155. it("handles onError with action", async () => {
  156. const executeActionFn = vi.fn();
  157. const error = new Error("Failed");
  158. await executeAction({
  159. action: {
  160. action: "test",
  161. params: {},
  162. onError: { action: "handleError" },
  163. },
  164. handler: vi.fn().mockRejectedValue(error),
  165. setState: vi.fn(),
  166. executeAction: executeActionFn,
  167. });
  168. expect(executeActionFn).toHaveBeenCalledWith("handleError");
  169. });
  170. it("re-throws error when no onError handler", async () => {
  171. const error = new Error("Unhandled");
  172. await expect(
  173. executeAction({
  174. action: { action: "test", params: {} },
  175. handler: vi.fn().mockRejectedValue(error),
  176. setState: vi.fn(),
  177. }),
  178. ).rejects.toThrow("Unhandled");
  179. });
  180. });
  181. describe("actionBinding helper", () => {
  182. describe("simple", () => {
  183. it("creates binding with action only", () => {
  184. const a = actionBinding.simple("navigate");
  185. expect(a.action).toBe("navigate");
  186. expect(a.params).toBeUndefined();
  187. });
  188. it("creates binding with params", () => {
  189. const a = actionBinding.simple("navigate", { url: "/home" });
  190. expect(a.action).toBe("navigate");
  191. expect(a.params).toEqual({ url: "/home" });
  192. });
  193. });
  194. describe("withConfirm", () => {
  195. it("creates binding with confirmation", () => {
  196. const a = actionBinding.withConfirm("delete", {
  197. title: "Confirm",
  198. message: "Are you sure?",
  199. });
  200. expect(a.action).toBe("delete");
  201. expect(a.confirm).toEqual({
  202. title: "Confirm",
  203. message: "Are you sure?",
  204. });
  205. });
  206. it("creates binding with confirmation and params", () => {
  207. const a = actionBinding.withConfirm(
  208. "delete",
  209. { title: "Delete", message: "Delete item?" },
  210. { id: 123 },
  211. );
  212. expect(a.action).toBe("delete");
  213. expect(a.params).toEqual({ id: 123 });
  214. expect(a.confirm?.title).toBe("Delete");
  215. });
  216. });
  217. describe("withSuccess", () => {
  218. it("creates binding with navigate success handler", () => {
  219. const a = actionBinding.withSuccess("save", { navigate: "/success" });
  220. expect(a.action).toBe("save");
  221. expect(a.onSuccess).toEqual({ navigate: "/success" });
  222. });
  223. it("creates binding with set success handler", () => {
  224. const a = actionBinding.withSuccess("save", { set: { saved: true } });
  225. expect(a.onSuccess).toEqual({ set: { saved: true } });
  226. });
  227. it("creates binding with success handler and params", () => {
  228. const a = actionBinding.withSuccess(
  229. "save",
  230. { navigate: "/done" },
  231. { data: "test" },
  232. );
  233. expect(a.params).toEqual({ data: "test" });
  234. expect(a.onSuccess).toEqual({ navigate: "/done" });
  235. });
  236. });
  237. });