| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- import { describe, it, expect } from "vitest";
- import {
- builtInValidationFunctions,
- runValidationCheck,
- runValidation,
- check,
- } from "./validation";
- describe("builtInValidationFunctions", () => {
- describe("required", () => {
- it("passes for non-empty values", () => {
- expect(builtInValidationFunctions.required("hello")).toBe(true);
- expect(builtInValidationFunctions.required(0)).toBe(true);
- expect(builtInValidationFunctions.required(false)).toBe(true);
- expect(builtInValidationFunctions.required(["item"])).toBe(true);
- expect(builtInValidationFunctions.required({ key: "value" })).toBe(true);
- });
- it("fails for empty values", () => {
- expect(builtInValidationFunctions.required("")).toBe(false);
- expect(builtInValidationFunctions.required(" ")).toBe(false);
- expect(builtInValidationFunctions.required(null)).toBe(false);
- expect(builtInValidationFunctions.required(undefined)).toBe(false);
- expect(builtInValidationFunctions.required([])).toBe(false);
- });
- });
- describe("email", () => {
- it("passes for valid emails", () => {
- expect(builtInValidationFunctions.email("test@example.com")).toBe(true);
- expect(builtInValidationFunctions.email("user.name@domain.co")).toBe(
- true,
- );
- expect(builtInValidationFunctions.email("a@b.c")).toBe(true);
- });
- it("fails for invalid emails", () => {
- expect(builtInValidationFunctions.email("invalid")).toBe(false);
- expect(builtInValidationFunctions.email("missing@domain")).toBe(false);
- expect(builtInValidationFunctions.email("@domain.com")).toBe(false);
- expect(builtInValidationFunctions.email("user@")).toBe(false);
- expect(builtInValidationFunctions.email(123)).toBe(false);
- });
- });
- describe("minLength", () => {
- it("passes when string meets minimum length", () => {
- expect(builtInValidationFunctions.minLength("hello", { min: 3 })).toBe(
- true,
- );
- expect(builtInValidationFunctions.minLength("abc", { min: 3 })).toBe(
- true,
- );
- expect(builtInValidationFunctions.minLength("abcdef", { min: 3 })).toBe(
- true,
- );
- });
- it("fails when string is too short", () => {
- expect(builtInValidationFunctions.minLength("hi", { min: 3 })).toBe(
- false,
- );
- expect(builtInValidationFunctions.minLength("", { min: 1 })).toBe(false);
- });
- it("fails for non-strings", () => {
- expect(builtInValidationFunctions.minLength(123, { min: 1 })).toBe(false);
- });
- it("fails when min is not provided", () => {
- expect(builtInValidationFunctions.minLength("hello", {})).toBe(false);
- });
- });
- describe("maxLength", () => {
- it("passes when string meets maximum length", () => {
- expect(builtInValidationFunctions.maxLength("hi", { max: 5 })).toBe(true);
- expect(builtInValidationFunctions.maxLength("hello", { max: 5 })).toBe(
- true,
- );
- });
- it("fails when string exceeds maximum", () => {
- expect(builtInValidationFunctions.maxLength("hello!", { max: 5 })).toBe(
- false,
- );
- });
- });
- describe("pattern", () => {
- it("passes when string matches pattern", () => {
- expect(
- builtInValidationFunctions.pattern("abc123", {
- pattern: "^[a-z0-9]+$",
- }),
- ).toBe(true);
- });
- it("fails when string does not match pattern", () => {
- expect(
- builtInValidationFunctions.pattern("ABC", { pattern: "^[a-z]+$" }),
- ).toBe(false);
- });
- it("fails for invalid regex pattern", () => {
- expect(
- builtInValidationFunctions.pattern("test", { pattern: "[invalid" }),
- ).toBe(false);
- });
- });
- describe("min", () => {
- it("passes when number meets minimum", () => {
- expect(builtInValidationFunctions.min(5, { min: 3 })).toBe(true);
- expect(builtInValidationFunctions.min(3, { min: 3 })).toBe(true);
- });
- it("fails when number is below minimum", () => {
- expect(builtInValidationFunctions.min(2, { min: 3 })).toBe(false);
- });
- it("fails for non-numbers", () => {
- expect(builtInValidationFunctions.min("5", { min: 3 })).toBe(false);
- });
- });
- describe("max", () => {
- it("passes when number meets maximum", () => {
- expect(builtInValidationFunctions.max(3, { max: 5 })).toBe(true);
- expect(builtInValidationFunctions.max(5, { max: 5 })).toBe(true);
- });
- it("fails when number exceeds maximum", () => {
- expect(builtInValidationFunctions.max(6, { max: 5 })).toBe(false);
- });
- });
- describe("numeric", () => {
- it("passes for numbers", () => {
- expect(builtInValidationFunctions.numeric(42)).toBe(true);
- expect(builtInValidationFunctions.numeric(3.14)).toBe(true);
- expect(builtInValidationFunctions.numeric(0)).toBe(true);
- });
- it("passes for numeric strings", () => {
- expect(builtInValidationFunctions.numeric("42")).toBe(true);
- expect(builtInValidationFunctions.numeric("3.14")).toBe(true);
- });
- it("fails for non-numeric values", () => {
- expect(builtInValidationFunctions.numeric("abc")).toBe(false);
- expect(builtInValidationFunctions.numeric(NaN)).toBe(false);
- expect(builtInValidationFunctions.numeric(null)).toBe(false);
- });
- });
- describe("url", () => {
- it("passes for valid URLs", () => {
- expect(builtInValidationFunctions.url("https://example.com")).toBe(true);
- expect(builtInValidationFunctions.url("http://localhost:3000")).toBe(
- true,
- );
- expect(
- builtInValidationFunctions.url("https://example.com/path?query=1"),
- ).toBe(true);
- });
- it("fails for invalid URLs", () => {
- expect(builtInValidationFunctions.url("not-a-url")).toBe(false);
- expect(builtInValidationFunctions.url("example.com")).toBe(false);
- });
- });
- describe("matches", () => {
- it("passes when values match", () => {
- expect(
- builtInValidationFunctions.matches("password", { other: "password" }),
- ).toBe(true);
- expect(builtInValidationFunctions.matches(123, { other: 123 })).toBe(
- true,
- );
- });
- it("fails when values do not match", () => {
- expect(
- builtInValidationFunctions.matches("password", { other: "different" }),
- ).toBe(false);
- });
- });
- describe("equalTo", () => {
- it("passes when values are equal", () => {
- expect(builtInValidationFunctions.equalTo("abc", { other: "abc" })).toBe(
- true,
- );
- });
- it("fails when values differ", () => {
- expect(builtInValidationFunctions.equalTo("abc", { other: "xyz" })).toBe(
- false,
- );
- });
- });
- describe("lessThan", () => {
- it("passes when value is less than other", () => {
- expect(builtInValidationFunctions.lessThan(3, { other: 5 })).toBe(true);
- });
- it("fails when value equals other", () => {
- expect(builtInValidationFunctions.lessThan(5, { other: 5 })).toBe(false);
- });
- it("fails when value is greater than other", () => {
- expect(builtInValidationFunctions.lessThan(7, { other: 5 })).toBe(false);
- });
- it("coerces numeric string vs number", () => {
- expect(builtInValidationFunctions.lessThan("3", { other: 5 })).toBe(true);
- });
- it("fails coercion when non-numeric string", () => {
- expect(builtInValidationFunctions.lessThan("abc", { other: 5 })).toBe(
- false,
- );
- });
- it("passes for string comparison (ISO dates)", () => {
- expect(
- builtInValidationFunctions.lessThan("2026-01-01", {
- other: "2026-06-15",
- }),
- ).toBe(true);
- });
- it("fails for equal strings", () => {
- expect(
- builtInValidationFunctions.lessThan("2026-01-01", {
- other: "2026-01-01",
- }),
- ).toBe(false);
- });
- it("returns false when value is empty string", () => {
- expect(builtInValidationFunctions.lessThan("", { other: 5 })).toBe(false);
- });
- it("returns false when other is empty string", () => {
- expect(builtInValidationFunctions.lessThan(3, { other: "" })).toBe(false);
- });
- it("returns false when value is empty string vs non-empty string", () => {
- expect(builtInValidationFunctions.lessThan("", { other: "abc" })).toBe(
- false,
- );
- });
- it("returns false when other is empty string vs non-empty string", () => {
- expect(builtInValidationFunctions.lessThan("abc", { other: "" })).toBe(
- false,
- );
- });
- it("returns false when other is null", () => {
- expect(builtInValidationFunctions.lessThan(3, { other: null })).toBe(
- false,
- );
- });
- it("returns false when value is null", () => {
- expect(builtInValidationFunctions.lessThan(null, { other: 5 })).toBe(
- false,
- );
- });
- it("returns false when other is undefined", () => {
- expect(builtInValidationFunctions.lessThan(3, { other: undefined })).toBe(
- false,
- );
- });
- });
- describe("greaterThan", () => {
- it("passes when value is greater than other", () => {
- expect(builtInValidationFunctions.greaterThan(7, { other: 5 })).toBe(
- true,
- );
- });
- it("fails when value equals other", () => {
- expect(builtInValidationFunctions.greaterThan(5, { other: 5 })).toBe(
- false,
- );
- });
- it("fails when value is less than other", () => {
- expect(builtInValidationFunctions.greaterThan(3, { other: 5 })).toBe(
- false,
- );
- });
- it("coerces numeric string vs number", () => {
- expect(builtInValidationFunctions.greaterThan("7", { other: 5 })).toBe(
- true,
- );
- });
- it("fails coercion when non-numeric string", () => {
- expect(builtInValidationFunctions.greaterThan("abc", { other: 5 })).toBe(
- false,
- );
- });
- it("passes for string comparison (ISO dates)", () => {
- expect(
- builtInValidationFunctions.greaterThan("2026-06-15", {
- other: "2026-01-01",
- }),
- ).toBe(true);
- });
- it("fails for lesser strings", () => {
- expect(
- builtInValidationFunctions.greaterThan("2026-01-01", {
- other: "2026-06-15",
- }),
- ).toBe(false);
- });
- it("returns false when value is empty string", () => {
- expect(builtInValidationFunctions.greaterThan("", { other: 5 })).toBe(
- false,
- );
- });
- it("returns false when other is empty string", () => {
- expect(builtInValidationFunctions.greaterThan(3, { other: "" })).toBe(
- false,
- );
- });
- it("returns false when value is empty string vs non-empty string", () => {
- expect(builtInValidationFunctions.greaterThan("", { other: "abc" })).toBe(
- false,
- );
- });
- it("returns false when other is empty string vs non-empty string", () => {
- expect(builtInValidationFunctions.greaterThan("abc", { other: "" })).toBe(
- false,
- );
- });
- it("returns false when other is null", () => {
- expect(builtInValidationFunctions.greaterThan(3, { other: null })).toBe(
- false,
- );
- });
- it("returns false when value is undefined", () => {
- expect(
- builtInValidationFunctions.greaterThan(undefined, { other: 5 }),
- ).toBe(false);
- });
- it("returns false when value is null", () => {
- expect(builtInValidationFunctions.greaterThan(null, { other: 5 })).toBe(
- false,
- );
- });
- });
- describe("requiredIf", () => {
- it("passes when condition is falsy (field not required)", () => {
- expect(builtInValidationFunctions.requiredIf("", { field: false })).toBe(
- true,
- );
- expect(builtInValidationFunctions.requiredIf("", { field: "" })).toBe(
- true,
- );
- expect(builtInValidationFunctions.requiredIf("", { field: null })).toBe(
- true,
- );
- expect(
- builtInValidationFunctions.requiredIf("", { field: undefined }),
- ).toBe(true);
- });
- it("fails when condition is truthy and value is empty", () => {
- expect(builtInValidationFunctions.requiredIf("", { field: true })).toBe(
- false,
- );
- expect(
- builtInValidationFunctions.requiredIf(null, { field: "yes" }),
- ).toBe(false);
- expect(
- builtInValidationFunctions.requiredIf(undefined, { field: 1 }),
- ).toBe(false);
- });
- it("passes when condition is truthy and value is present", () => {
- expect(
- builtInValidationFunctions.requiredIf("hello", { field: true }),
- ).toBe(true);
- expect(builtInValidationFunctions.requiredIf(42, { field: true })).toBe(
- true,
- );
- });
- });
- });
- describe("runValidationCheck", () => {
- it("runs a validation check and returns result", () => {
- const result = runValidationCheck(
- { type: "required", message: "Required" },
- { value: "hello", stateModel: {} },
- );
- expect(result.type).toBe("required");
- expect(result.valid).toBe(true);
- expect(result.message).toBe("Required");
- });
- it("resolves dynamic args from stateModel", () => {
- const result = runValidationCheck(
- {
- type: "minLength",
- args: { min: { $state: "/minLen" } },
- message: "Too short",
- },
- { value: "hi", stateModel: { minLen: 5 } },
- );
- expect(result.valid).toBe(false);
- });
- it("returns valid for unknown functions with warning", () => {
- const result = runValidationCheck(
- { type: "unknownFunction", message: "Unknown" },
- { value: "test", stateModel: {} },
- );
- expect(result.valid).toBe(true);
- });
- it("uses custom validation functions", () => {
- const customFunctions = {
- startsWithA: (value: unknown) =>
- typeof value === "string" && value.startsWith("A"),
- };
- const result = runValidationCheck(
- { type: "startsWithA", message: "Must start with A" },
- { value: "Apple", stateModel: {}, customFunctions },
- );
- expect(result.valid).toBe(true);
- });
- });
- describe("runValidation", () => {
- it("runs all validation checks", () => {
- const result = runValidation(
- {
- checks: [
- { type: "required", message: "Required" },
- { type: "email", message: "Invalid email" },
- ],
- },
- { value: "test@example.com", stateModel: {} },
- );
- expect(result.valid).toBe(true);
- expect(result.errors).toHaveLength(0);
- expect(result.checks).toHaveLength(2);
- });
- it("collects all errors", () => {
- const result = runValidation(
- {
- checks: [
- { type: "required", message: "Required" },
- { type: "email", message: "Invalid email" },
- ],
- },
- { value: "", stateModel: {} },
- );
- expect(result.valid).toBe(false);
- expect(result.errors).toContain("Required");
- expect(result.errors).toContain("Invalid email");
- });
- it("skips validation when enabled condition is false", () => {
- const result = runValidation(
- {
- checks: [{ type: "required", message: "Required" }],
- enabled: { $state: "/enabled", eq: true }, // False because /enabled is not set
- },
- { value: "", stateModel: {} },
- );
- expect(result.valid).toBe(true);
- expect(result.checks).toHaveLength(0);
- });
- it("runs validation when enabled condition is true", () => {
- const result = runValidation(
- {
- checks: [{ type: "required", message: "Required" }],
- enabled: { $state: "/enabled" }, // True because /enabled is truthy
- },
- { value: "", stateModel: { enabled: true } },
- );
- expect(result.valid).toBe(false);
- });
- it("returns valid when no checks defined", () => {
- const result = runValidation({}, { value: "", stateModel: {} });
- expect(result.valid).toBe(true);
- expect(result.checks).toHaveLength(0);
- });
- });
- describe("check helper", () => {
- describe("required", () => {
- it("creates required check with default message", () => {
- const c = check.required();
- expect(c.type).toBe("required");
- expect(c.message).toBe("This field is required");
- });
- it("creates required check with custom message", () => {
- const c = check.required("Custom message");
- expect(c.message).toBe("Custom message");
- });
- });
- describe("email", () => {
- it("creates email check with default message", () => {
- const c = check.email();
- expect(c.type).toBe("email");
- expect(c.message).toBe("Invalid email address");
- });
- });
- describe("minLength", () => {
- it("creates minLength check with args", () => {
- const c = check.minLength(5, "Too short");
- expect(c.type).toBe("minLength");
- expect(c.args).toEqual({ min: 5 });
- expect(c.message).toBe("Too short");
- });
- it("uses default message", () => {
- const c = check.minLength(3);
- expect(c.message).toBe("Must be at least 3 characters");
- });
- });
- describe("maxLength", () => {
- it("creates maxLength check with args", () => {
- const c = check.maxLength(100);
- expect(c.type).toBe("maxLength");
- expect(c.args).toEqual({ max: 100 });
- });
- });
- describe("pattern", () => {
- it("creates pattern check", () => {
- const c = check.pattern("^[a-z]+$", "Letters only");
- expect(c.type).toBe("pattern");
- expect(c.args).toEqual({ pattern: "^[a-z]+$" });
- expect(c.message).toBe("Letters only");
- });
- });
- describe("min", () => {
- it("creates min check", () => {
- const c = check.min(0, "Must be positive");
- expect(c.type).toBe("min");
- expect(c.args).toEqual({ min: 0 });
- });
- });
- describe("max", () => {
- it("creates max check", () => {
- const c = check.max(100);
- expect(c.type).toBe("max");
- expect(c.args).toEqual({ max: 100 });
- });
- });
- describe("url", () => {
- it("creates url check", () => {
- const c = check.url("Must be a URL");
- expect(c.type).toBe("url");
- expect(c.message).toBe("Must be a URL");
- });
- });
- describe("numeric", () => {
- it("creates numeric check with default message", () => {
- const c = check.numeric();
- expect(c.type).toBe("numeric");
- expect(c.message).toBe("Must be a number");
- });
- it("creates numeric check with custom message", () => {
- const c = check.numeric("Numbers only");
- expect(c.type).toBe("numeric");
- expect(c.message).toBe("Numbers only");
- });
- });
- describe("matches", () => {
- it("creates matches check with path reference", () => {
- const c = check.matches("/password", "Passwords must match");
- expect(c.type).toBe("matches");
- expect(c.args).toEqual({ other: { $state: "/password" } });
- expect(c.message).toBe("Passwords must match");
- });
- });
- describe("equalTo", () => {
- it("creates equalTo check with path reference", () => {
- const c = check.equalTo("/email", "Emails must match");
- expect(c.type).toBe("equalTo");
- expect(c.args).toEqual({ other: { $state: "/email" } });
- expect(c.message).toBe("Emails must match");
- });
- });
- describe("lessThan", () => {
- it("creates lessThan check with path reference", () => {
- const c = check.lessThan("/maxValue", "Must be less");
- expect(c.type).toBe("lessThan");
- expect(c.args).toEqual({ other: { $state: "/maxValue" } });
- expect(c.message).toBe("Must be less");
- });
- });
- describe("greaterThan", () => {
- it("creates greaterThan check with path reference", () => {
- const c = check.greaterThan("/minValue");
- expect(c.type).toBe("greaterThan");
- expect(c.args).toEqual({ other: { $state: "/minValue" } });
- });
- });
- describe("requiredIf", () => {
- it("creates requiredIf check with path reference", () => {
- const c = check.requiredIf("/toggle", "Required when toggle is on");
- expect(c.type).toBe("requiredIf");
- expect(c.args).toEqual({ field: { $state: "/toggle" } });
- expect(c.message).toBe("Required when toggle is on");
- });
- });
- });
- // =============================================================================
- // Deep arg resolution in runValidationCheck
- // =============================================================================
- describe("deep arg resolution", () => {
- it("resolves nested $state refs in validation args", () => {
- const result = runValidationCheck(
- {
- type: "matches",
- args: { other: { $state: "/form/password" } },
- message: "Passwords must match",
- },
- {
- value: "secret123",
- stateModel: { form: { password: "secret123" } },
- },
- );
- expect(result.valid).toBe(true);
- });
- it("resolves $state in cross-field lessThan check", () => {
- const result = runValidationCheck(
- {
- type: "lessThan",
- args: { other: { $state: "/form/maxPrice" } },
- message: "Must be less than max price",
- },
- {
- value: 50,
- stateModel: { form: { maxPrice: 100 } },
- },
- );
- expect(result.valid).toBe(true);
- });
- it("resolves $state in requiredIf check", () => {
- const result = runValidationCheck(
- {
- type: "requiredIf",
- args: { field: { $state: "/form/enableEmail" } },
- message: "Email is required",
- },
- {
- value: "",
- stateModel: { form: { enableEmail: true } },
- },
- );
- expect(result.valid).toBe(false);
- });
- it("passes requiredIf when condition is false", () => {
- const result = runValidationCheck(
- {
- type: "requiredIf",
- args: { field: { $state: "/form/enableEmail" } },
- message: "Email is required",
- },
- {
- value: "",
- stateModel: { form: { enableEmail: false } },
- },
- );
- expect(result.valid).toBe(true);
- });
- });
|