validation.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. import { describe, it, expect } from "vitest";
  2. import {
  3. builtInValidationFunctions,
  4. runValidationCheck,
  5. runValidation,
  6. check,
  7. } from "./validation";
  8. describe("builtInValidationFunctions", () => {
  9. describe("required", () => {
  10. it("passes for non-empty values", () => {
  11. expect(builtInValidationFunctions.required("hello")).toBe(true);
  12. expect(builtInValidationFunctions.required(0)).toBe(true);
  13. expect(builtInValidationFunctions.required(false)).toBe(true);
  14. expect(builtInValidationFunctions.required(["item"])).toBe(true);
  15. expect(builtInValidationFunctions.required({ key: "value" })).toBe(true);
  16. });
  17. it("fails for empty values", () => {
  18. expect(builtInValidationFunctions.required("")).toBe(false);
  19. expect(builtInValidationFunctions.required(" ")).toBe(false);
  20. expect(builtInValidationFunctions.required(null)).toBe(false);
  21. expect(builtInValidationFunctions.required(undefined)).toBe(false);
  22. expect(builtInValidationFunctions.required([])).toBe(false);
  23. });
  24. });
  25. describe("email", () => {
  26. it("passes for valid emails", () => {
  27. expect(builtInValidationFunctions.email("test@example.com")).toBe(true);
  28. expect(builtInValidationFunctions.email("user.name@domain.co")).toBe(
  29. true,
  30. );
  31. expect(builtInValidationFunctions.email("a@b.c")).toBe(true);
  32. });
  33. it("fails for invalid emails", () => {
  34. expect(builtInValidationFunctions.email("invalid")).toBe(false);
  35. expect(builtInValidationFunctions.email("missing@domain")).toBe(false);
  36. expect(builtInValidationFunctions.email("@domain.com")).toBe(false);
  37. expect(builtInValidationFunctions.email("user@")).toBe(false);
  38. expect(builtInValidationFunctions.email(123)).toBe(false);
  39. });
  40. });
  41. describe("minLength", () => {
  42. it("passes when string meets minimum length", () => {
  43. expect(builtInValidationFunctions.minLength("hello", { min: 3 })).toBe(
  44. true,
  45. );
  46. expect(builtInValidationFunctions.minLength("abc", { min: 3 })).toBe(
  47. true,
  48. );
  49. expect(builtInValidationFunctions.minLength("abcdef", { min: 3 })).toBe(
  50. true,
  51. );
  52. });
  53. it("fails when string is too short", () => {
  54. expect(builtInValidationFunctions.minLength("hi", { min: 3 })).toBe(
  55. false,
  56. );
  57. expect(builtInValidationFunctions.minLength("", { min: 1 })).toBe(false);
  58. });
  59. it("fails for non-strings", () => {
  60. expect(builtInValidationFunctions.minLength(123, { min: 1 })).toBe(false);
  61. });
  62. it("fails when min is not provided", () => {
  63. expect(builtInValidationFunctions.minLength("hello", {})).toBe(false);
  64. });
  65. });
  66. describe("maxLength", () => {
  67. it("passes when string meets maximum length", () => {
  68. expect(builtInValidationFunctions.maxLength("hi", { max: 5 })).toBe(true);
  69. expect(builtInValidationFunctions.maxLength("hello", { max: 5 })).toBe(
  70. true,
  71. );
  72. });
  73. it("fails when string exceeds maximum", () => {
  74. expect(builtInValidationFunctions.maxLength("hello!", { max: 5 })).toBe(
  75. false,
  76. );
  77. });
  78. });
  79. describe("pattern", () => {
  80. it("passes when string matches pattern", () => {
  81. expect(
  82. builtInValidationFunctions.pattern("abc123", {
  83. pattern: "^[a-z0-9]+$",
  84. }),
  85. ).toBe(true);
  86. });
  87. it("fails when string does not match pattern", () => {
  88. expect(
  89. builtInValidationFunctions.pattern("ABC", { pattern: "^[a-z]+$" }),
  90. ).toBe(false);
  91. });
  92. it("fails for invalid regex pattern", () => {
  93. expect(
  94. builtInValidationFunctions.pattern("test", { pattern: "[invalid" }),
  95. ).toBe(false);
  96. });
  97. });
  98. describe("min", () => {
  99. it("passes when number meets minimum", () => {
  100. expect(builtInValidationFunctions.min(5, { min: 3 })).toBe(true);
  101. expect(builtInValidationFunctions.min(3, { min: 3 })).toBe(true);
  102. });
  103. it("fails when number is below minimum", () => {
  104. expect(builtInValidationFunctions.min(2, { min: 3 })).toBe(false);
  105. });
  106. it("fails for non-numbers", () => {
  107. expect(builtInValidationFunctions.min("5", { min: 3 })).toBe(false);
  108. });
  109. });
  110. describe("max", () => {
  111. it("passes when number meets maximum", () => {
  112. expect(builtInValidationFunctions.max(3, { max: 5 })).toBe(true);
  113. expect(builtInValidationFunctions.max(5, { max: 5 })).toBe(true);
  114. });
  115. it("fails when number exceeds maximum", () => {
  116. expect(builtInValidationFunctions.max(6, { max: 5 })).toBe(false);
  117. });
  118. });
  119. describe("numeric", () => {
  120. it("passes for numbers", () => {
  121. expect(builtInValidationFunctions.numeric(42)).toBe(true);
  122. expect(builtInValidationFunctions.numeric(3.14)).toBe(true);
  123. expect(builtInValidationFunctions.numeric(0)).toBe(true);
  124. });
  125. it("passes for numeric strings", () => {
  126. expect(builtInValidationFunctions.numeric("42")).toBe(true);
  127. expect(builtInValidationFunctions.numeric("3.14")).toBe(true);
  128. });
  129. it("fails for non-numeric values", () => {
  130. expect(builtInValidationFunctions.numeric("abc")).toBe(false);
  131. expect(builtInValidationFunctions.numeric(NaN)).toBe(false);
  132. expect(builtInValidationFunctions.numeric(null)).toBe(false);
  133. });
  134. });
  135. describe("url", () => {
  136. it("passes for valid URLs", () => {
  137. expect(builtInValidationFunctions.url("https://example.com")).toBe(true);
  138. expect(builtInValidationFunctions.url("http://localhost:3000")).toBe(
  139. true,
  140. );
  141. expect(
  142. builtInValidationFunctions.url("https://example.com/path?query=1"),
  143. ).toBe(true);
  144. });
  145. it("fails for invalid URLs", () => {
  146. expect(builtInValidationFunctions.url("not-a-url")).toBe(false);
  147. expect(builtInValidationFunctions.url("example.com")).toBe(false);
  148. });
  149. });
  150. describe("matches", () => {
  151. it("passes when values match", () => {
  152. expect(
  153. builtInValidationFunctions.matches("password", { other: "password" }),
  154. ).toBe(true);
  155. expect(builtInValidationFunctions.matches(123, { other: 123 })).toBe(
  156. true,
  157. );
  158. });
  159. it("fails when values do not match", () => {
  160. expect(
  161. builtInValidationFunctions.matches("password", { other: "different" }),
  162. ).toBe(false);
  163. });
  164. });
  165. describe("equalTo", () => {
  166. it("passes when values are equal", () => {
  167. expect(builtInValidationFunctions.equalTo("abc", { other: "abc" })).toBe(
  168. true,
  169. );
  170. });
  171. it("fails when values differ", () => {
  172. expect(builtInValidationFunctions.equalTo("abc", { other: "xyz" })).toBe(
  173. false,
  174. );
  175. });
  176. });
  177. describe("lessThan", () => {
  178. it("passes when value is less than other", () => {
  179. expect(builtInValidationFunctions.lessThan(3, { other: 5 })).toBe(true);
  180. });
  181. it("fails when value equals other", () => {
  182. expect(builtInValidationFunctions.lessThan(5, { other: 5 })).toBe(false);
  183. });
  184. it("fails when value is greater than other", () => {
  185. expect(builtInValidationFunctions.lessThan(7, { other: 5 })).toBe(false);
  186. });
  187. it("coerces numeric string vs number", () => {
  188. expect(builtInValidationFunctions.lessThan("3", { other: 5 })).toBe(true);
  189. });
  190. it("fails coercion when non-numeric string", () => {
  191. expect(builtInValidationFunctions.lessThan("abc", { other: 5 })).toBe(
  192. false,
  193. );
  194. });
  195. it("passes for string comparison (ISO dates)", () => {
  196. expect(
  197. builtInValidationFunctions.lessThan("2026-01-01", {
  198. other: "2026-06-15",
  199. }),
  200. ).toBe(true);
  201. });
  202. it("fails for equal strings", () => {
  203. expect(
  204. builtInValidationFunctions.lessThan("2026-01-01", {
  205. other: "2026-01-01",
  206. }),
  207. ).toBe(false);
  208. });
  209. it("returns false when value is empty string", () => {
  210. expect(builtInValidationFunctions.lessThan("", { other: 5 })).toBe(false);
  211. });
  212. it("returns false when other is empty string", () => {
  213. expect(builtInValidationFunctions.lessThan(3, { other: "" })).toBe(false);
  214. });
  215. it("returns false when value is empty string vs non-empty string", () => {
  216. expect(builtInValidationFunctions.lessThan("", { other: "abc" })).toBe(
  217. false,
  218. );
  219. });
  220. it("returns false when other is empty string vs non-empty string", () => {
  221. expect(builtInValidationFunctions.lessThan("abc", { other: "" })).toBe(
  222. false,
  223. );
  224. });
  225. it("returns false when other is null", () => {
  226. expect(builtInValidationFunctions.lessThan(3, { other: null })).toBe(
  227. false,
  228. );
  229. });
  230. it("returns false when value is null", () => {
  231. expect(builtInValidationFunctions.lessThan(null, { other: 5 })).toBe(
  232. false,
  233. );
  234. });
  235. it("returns false when other is undefined", () => {
  236. expect(builtInValidationFunctions.lessThan(3, { other: undefined })).toBe(
  237. false,
  238. );
  239. });
  240. });
  241. describe("greaterThan", () => {
  242. it("passes when value is greater than other", () => {
  243. expect(builtInValidationFunctions.greaterThan(7, { other: 5 })).toBe(
  244. true,
  245. );
  246. });
  247. it("fails when value equals other", () => {
  248. expect(builtInValidationFunctions.greaterThan(5, { other: 5 })).toBe(
  249. false,
  250. );
  251. });
  252. it("fails when value is less than other", () => {
  253. expect(builtInValidationFunctions.greaterThan(3, { other: 5 })).toBe(
  254. false,
  255. );
  256. });
  257. it("coerces numeric string vs number", () => {
  258. expect(builtInValidationFunctions.greaterThan("7", { other: 5 })).toBe(
  259. true,
  260. );
  261. });
  262. it("fails coercion when non-numeric string", () => {
  263. expect(builtInValidationFunctions.greaterThan("abc", { other: 5 })).toBe(
  264. false,
  265. );
  266. });
  267. it("passes for string comparison (ISO dates)", () => {
  268. expect(
  269. builtInValidationFunctions.greaterThan("2026-06-15", {
  270. other: "2026-01-01",
  271. }),
  272. ).toBe(true);
  273. });
  274. it("fails for lesser strings", () => {
  275. expect(
  276. builtInValidationFunctions.greaterThan("2026-01-01", {
  277. other: "2026-06-15",
  278. }),
  279. ).toBe(false);
  280. });
  281. it("returns false when value is empty string", () => {
  282. expect(builtInValidationFunctions.greaterThan("", { other: 5 })).toBe(
  283. false,
  284. );
  285. });
  286. it("returns false when other is empty string", () => {
  287. expect(builtInValidationFunctions.greaterThan(3, { other: "" })).toBe(
  288. false,
  289. );
  290. });
  291. it("returns false when value is empty string vs non-empty string", () => {
  292. expect(builtInValidationFunctions.greaterThan("", { other: "abc" })).toBe(
  293. false,
  294. );
  295. });
  296. it("returns false when other is empty string vs non-empty string", () => {
  297. expect(builtInValidationFunctions.greaterThan("abc", { other: "" })).toBe(
  298. false,
  299. );
  300. });
  301. it("returns false when other is null", () => {
  302. expect(builtInValidationFunctions.greaterThan(3, { other: null })).toBe(
  303. false,
  304. );
  305. });
  306. it("returns false when value is undefined", () => {
  307. expect(
  308. builtInValidationFunctions.greaterThan(undefined, { other: 5 }),
  309. ).toBe(false);
  310. });
  311. it("returns false when value is null", () => {
  312. expect(builtInValidationFunctions.greaterThan(null, { other: 5 })).toBe(
  313. false,
  314. );
  315. });
  316. });
  317. describe("requiredIf", () => {
  318. it("passes when condition is falsy (field not required)", () => {
  319. expect(builtInValidationFunctions.requiredIf("", { field: false })).toBe(
  320. true,
  321. );
  322. expect(builtInValidationFunctions.requiredIf("", { field: "" })).toBe(
  323. true,
  324. );
  325. expect(builtInValidationFunctions.requiredIf("", { field: null })).toBe(
  326. true,
  327. );
  328. expect(
  329. builtInValidationFunctions.requiredIf("", { field: undefined }),
  330. ).toBe(true);
  331. });
  332. it("fails when condition is truthy and value is empty", () => {
  333. expect(builtInValidationFunctions.requiredIf("", { field: true })).toBe(
  334. false,
  335. );
  336. expect(
  337. builtInValidationFunctions.requiredIf(null, { field: "yes" }),
  338. ).toBe(false);
  339. expect(
  340. builtInValidationFunctions.requiredIf(undefined, { field: 1 }),
  341. ).toBe(false);
  342. });
  343. it("passes when condition is truthy and value is present", () => {
  344. expect(
  345. builtInValidationFunctions.requiredIf("hello", { field: true }),
  346. ).toBe(true);
  347. expect(builtInValidationFunctions.requiredIf(42, { field: true })).toBe(
  348. true,
  349. );
  350. });
  351. });
  352. });
  353. describe("runValidationCheck", () => {
  354. it("runs a validation check and returns result", () => {
  355. const result = runValidationCheck(
  356. { type: "required", message: "Required" },
  357. { value: "hello", stateModel: {} },
  358. );
  359. expect(result.type).toBe("required");
  360. expect(result.valid).toBe(true);
  361. expect(result.message).toBe("Required");
  362. });
  363. it("resolves dynamic args from stateModel", () => {
  364. const result = runValidationCheck(
  365. {
  366. type: "minLength",
  367. args: { min: { $state: "/minLen" } },
  368. message: "Too short",
  369. },
  370. { value: "hi", stateModel: { minLen: 5 } },
  371. );
  372. expect(result.valid).toBe(false);
  373. });
  374. it("returns valid for unknown functions with warning", () => {
  375. const result = runValidationCheck(
  376. { type: "unknownFunction", message: "Unknown" },
  377. { value: "test", stateModel: {} },
  378. );
  379. expect(result.valid).toBe(true);
  380. });
  381. it("uses custom validation functions", () => {
  382. const customFunctions = {
  383. startsWithA: (value: unknown) =>
  384. typeof value === "string" && value.startsWith("A"),
  385. };
  386. const result = runValidationCheck(
  387. { type: "startsWithA", message: "Must start with A" },
  388. { value: "Apple", stateModel: {}, customFunctions },
  389. );
  390. expect(result.valid).toBe(true);
  391. });
  392. });
  393. describe("runValidation", () => {
  394. it("runs all validation checks", () => {
  395. const result = runValidation(
  396. {
  397. checks: [
  398. { type: "required", message: "Required" },
  399. { type: "email", message: "Invalid email" },
  400. ],
  401. },
  402. { value: "test@example.com", stateModel: {} },
  403. );
  404. expect(result.valid).toBe(true);
  405. expect(result.errors).toHaveLength(0);
  406. expect(result.checks).toHaveLength(2);
  407. });
  408. it("collects all errors", () => {
  409. const result = runValidation(
  410. {
  411. checks: [
  412. { type: "required", message: "Required" },
  413. { type: "email", message: "Invalid email" },
  414. ],
  415. },
  416. { value: "", stateModel: {} },
  417. );
  418. expect(result.valid).toBe(false);
  419. expect(result.errors).toContain("Required");
  420. expect(result.errors).toContain("Invalid email");
  421. });
  422. it("skips validation when enabled condition is false", () => {
  423. const result = runValidation(
  424. {
  425. checks: [{ type: "required", message: "Required" }],
  426. enabled: { $state: "/enabled", eq: true }, // False because /enabled is not set
  427. },
  428. { value: "", stateModel: {} },
  429. );
  430. expect(result.valid).toBe(true);
  431. expect(result.checks).toHaveLength(0);
  432. });
  433. it("runs validation when enabled condition is true", () => {
  434. const result = runValidation(
  435. {
  436. checks: [{ type: "required", message: "Required" }],
  437. enabled: { $state: "/enabled" }, // True because /enabled is truthy
  438. },
  439. { value: "", stateModel: { enabled: true } },
  440. );
  441. expect(result.valid).toBe(false);
  442. });
  443. it("returns valid when no checks defined", () => {
  444. const result = runValidation({}, { value: "", stateModel: {} });
  445. expect(result.valid).toBe(true);
  446. expect(result.checks).toHaveLength(0);
  447. });
  448. });
  449. describe("check helper", () => {
  450. describe("required", () => {
  451. it("creates required check with default message", () => {
  452. const c = check.required();
  453. expect(c.type).toBe("required");
  454. expect(c.message).toBe("This field is required");
  455. });
  456. it("creates required check with custom message", () => {
  457. const c = check.required("Custom message");
  458. expect(c.message).toBe("Custom message");
  459. });
  460. });
  461. describe("email", () => {
  462. it("creates email check with default message", () => {
  463. const c = check.email();
  464. expect(c.type).toBe("email");
  465. expect(c.message).toBe("Invalid email address");
  466. });
  467. });
  468. describe("minLength", () => {
  469. it("creates minLength check with args", () => {
  470. const c = check.minLength(5, "Too short");
  471. expect(c.type).toBe("minLength");
  472. expect(c.args).toEqual({ min: 5 });
  473. expect(c.message).toBe("Too short");
  474. });
  475. it("uses default message", () => {
  476. const c = check.minLength(3);
  477. expect(c.message).toBe("Must be at least 3 characters");
  478. });
  479. });
  480. describe("maxLength", () => {
  481. it("creates maxLength check with args", () => {
  482. const c = check.maxLength(100);
  483. expect(c.type).toBe("maxLength");
  484. expect(c.args).toEqual({ max: 100 });
  485. });
  486. });
  487. describe("pattern", () => {
  488. it("creates pattern check", () => {
  489. const c = check.pattern("^[a-z]+$", "Letters only");
  490. expect(c.type).toBe("pattern");
  491. expect(c.args).toEqual({ pattern: "^[a-z]+$" });
  492. expect(c.message).toBe("Letters only");
  493. });
  494. });
  495. describe("min", () => {
  496. it("creates min check", () => {
  497. const c = check.min(0, "Must be positive");
  498. expect(c.type).toBe("min");
  499. expect(c.args).toEqual({ min: 0 });
  500. });
  501. });
  502. describe("max", () => {
  503. it("creates max check", () => {
  504. const c = check.max(100);
  505. expect(c.type).toBe("max");
  506. expect(c.args).toEqual({ max: 100 });
  507. });
  508. });
  509. describe("url", () => {
  510. it("creates url check", () => {
  511. const c = check.url("Must be a URL");
  512. expect(c.type).toBe("url");
  513. expect(c.message).toBe("Must be a URL");
  514. });
  515. });
  516. describe("numeric", () => {
  517. it("creates numeric check with default message", () => {
  518. const c = check.numeric();
  519. expect(c.type).toBe("numeric");
  520. expect(c.message).toBe("Must be a number");
  521. });
  522. it("creates numeric check with custom message", () => {
  523. const c = check.numeric("Numbers only");
  524. expect(c.type).toBe("numeric");
  525. expect(c.message).toBe("Numbers only");
  526. });
  527. });
  528. describe("matches", () => {
  529. it("creates matches check with path reference", () => {
  530. const c = check.matches("/password", "Passwords must match");
  531. expect(c.type).toBe("matches");
  532. expect(c.args).toEqual({ other: { $state: "/password" } });
  533. expect(c.message).toBe("Passwords must match");
  534. });
  535. });
  536. describe("equalTo", () => {
  537. it("creates equalTo check with path reference", () => {
  538. const c = check.equalTo("/email", "Emails must match");
  539. expect(c.type).toBe("equalTo");
  540. expect(c.args).toEqual({ other: { $state: "/email" } });
  541. expect(c.message).toBe("Emails must match");
  542. });
  543. });
  544. describe("lessThan", () => {
  545. it("creates lessThan check with path reference", () => {
  546. const c = check.lessThan("/maxValue", "Must be less");
  547. expect(c.type).toBe("lessThan");
  548. expect(c.args).toEqual({ other: { $state: "/maxValue" } });
  549. expect(c.message).toBe("Must be less");
  550. });
  551. });
  552. describe("greaterThan", () => {
  553. it("creates greaterThan check with path reference", () => {
  554. const c = check.greaterThan("/minValue");
  555. expect(c.type).toBe("greaterThan");
  556. expect(c.args).toEqual({ other: { $state: "/minValue" } });
  557. });
  558. });
  559. describe("requiredIf", () => {
  560. it("creates requiredIf check with path reference", () => {
  561. const c = check.requiredIf("/toggle", "Required when toggle is on");
  562. expect(c.type).toBe("requiredIf");
  563. expect(c.args).toEqual({ field: { $state: "/toggle" } });
  564. expect(c.message).toBe("Required when toggle is on");
  565. });
  566. });
  567. });
  568. // =============================================================================
  569. // Deep arg resolution in runValidationCheck
  570. // =============================================================================
  571. describe("deep arg resolution", () => {
  572. it("resolves nested $state refs in validation args", () => {
  573. const result = runValidationCheck(
  574. {
  575. type: "matches",
  576. args: { other: { $state: "/form/password" } },
  577. message: "Passwords must match",
  578. },
  579. {
  580. value: "secret123",
  581. stateModel: { form: { password: "secret123" } },
  582. },
  583. );
  584. expect(result.valid).toBe(true);
  585. });
  586. it("resolves $state in cross-field lessThan check", () => {
  587. const result = runValidationCheck(
  588. {
  589. type: "lessThan",
  590. args: { other: { $state: "/form/maxPrice" } },
  591. message: "Must be less than max price",
  592. },
  593. {
  594. value: 50,
  595. stateModel: { form: { maxPrice: 100 } },
  596. },
  597. );
  598. expect(result.valid).toBe(true);
  599. });
  600. it("resolves $state in requiredIf check", () => {
  601. const result = runValidationCheck(
  602. {
  603. type: "requiredIf",
  604. args: { field: { $state: "/form/enableEmail" } },
  605. message: "Email is required",
  606. },
  607. {
  608. value: "",
  609. stateModel: { form: { enableEmail: true } },
  610. },
  611. );
  612. expect(result.valid).toBe(false);
  613. });
  614. it("passes requiredIf when condition is false", () => {
  615. const result = runValidationCheck(
  616. {
  617. type: "requiredIf",
  618. args: { field: { $state: "/form/enableEmail" } },
  619. message: "Email is required",
  620. },
  621. {
  622. value: "",
  623. stateModel: { form: { enableEmail: false } },
  624. },
  625. );
  626. expect(result.valid).toBe(true);
  627. });
  628. });