refine.test.ts•8.17 kB
// @ts-ignore TS6133
import { expect, test } from "vitest";
import * as z from "zod/v3";
import { ZodIssueCode } from "../ZodError.js";
import { util } from "../helpers/util.js";
test("refinement", () => {
  const obj1 = z.object({
    first: z.string(),
    second: z.string(),
  });
  const obj2 = obj1.partial().strict();
  const obj3 = obj2.refine((data) => data.first || data.second, "Either first or second should be filled in.");
  expect(obj1 === (obj2 as any)).toEqual(false);
  expect(obj2 === (obj3 as any)).toEqual(false);
  expect(() => obj1.parse({})).toThrow();
  expect(() => obj2.parse({ third: "adsf" })).toThrow();
  expect(() => obj3.parse({})).toThrow();
  obj3.parse({ first: "a" });
  obj3.parse({ second: "a" });
  obj3.parse({ first: "a", second: "a" });
});
test("refinement 2", () => {
  const validationSchema = z
    .object({
      email: z.string().email(),
      password: z.string(),
      confirmPassword: z.string(),
    })
    .refine((data) => data.password === data.confirmPassword, "Both password and confirmation must match");
  expect(() =>
    validationSchema.parse({
      email: "aaaa@gmail.com",
      password: "aaaaaaaa",
      confirmPassword: "bbbbbbbb",
    })
  ).toThrow();
});
test("refinement type guard", () => {
  const validationSchema = z.object({
    a: z.string().refine((s): s is "a" => s === "a"),
  });
  type Input = z.input<typeof validationSchema>;
  type Schema = z.infer<typeof validationSchema>;
  util.assertEqual<"a", Input["a"]>(false);
  util.assertEqual<string, Input["a"]>(true);
  util.assertEqual<"a", Schema["a"]>(true);
  util.assertEqual<string, Schema["a"]>(false);
});
test("refinement Promise", async () => {
  const validationSchema = z
    .object({
      email: z.string().email(),
      password: z.string(),
      confirmPassword: z.string(),
    })
    .refine(
      (data) => Promise.resolve().then(() => data.password === data.confirmPassword),
      "Both password and confirmation must match"
    );
  await validationSchema.parseAsync({
    email: "aaaa@gmail.com",
    password: "password",
    confirmPassword: "password",
  });
});
test("custom path", async () => {
  const result = await z
    .object({
      password: z.string(),
      confirm: z.string(),
    })
    .refine((data) => data.confirm === data.password, { path: ["confirm"] })
    .spa({ password: "asdf", confirm: "qewr" });
  expect(result.success).toEqual(false);
  if (!result.success) {
    expect(result.error.issues[0].path).toEqual(["confirm"]);
  }
});
test("use path in refinement context", async () => {
  const noNested = z.string()._refinement((_val, ctx) => {
    if (ctx.path.length > 0) {
      ctx.addIssue({
        code: ZodIssueCode.custom,
        message: `schema cannot be nested. path: ${ctx.path.join(".")}`,
      });
      return false;
    } else {
      return true;
    }
  });
  const data = z.object({
    foo: noNested,
  });
  const t1 = await noNested.spa("asdf");
  const t2 = await data.spa({ foo: "asdf" });
  expect(t1.success).toBe(true);
  expect(t2.success).toBe(false);
  if (t2.success === false) {
    expect(t2.error.issues[0].message).toEqual("schema cannot be nested. path: foo");
  }
});
test("superRefine", () => {
  const Strings = z.array(z.string()).superRefine((val, ctx) => {
    if (val.length > 3) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_big,
        maximum: 3,
        type: "array",
        inclusive: true,
        exact: true,
        message: "Too many items 😡",
      });
    }
    if (val.length !== new Set(val).size) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `No duplicates allowed.`,
      });
    }
  });
  const result = Strings.safeParse(["asfd", "asfd", "asfd", "asfd"]);
  expect(result.success).toEqual(false);
  if (!result.success) expect(result.error.issues.length).toEqual(2);
  Strings.parse(["asfd", "qwer"]);
});
test("superRefine async", async () => {
  const Strings = z.array(z.string()).superRefine(async (val, ctx) => {
    if (val.length > 3) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_big,
        maximum: 3,
        type: "array",
        inclusive: true,
        exact: true,
        message: "Too many items 😡",
      });
    }
    if (val.length !== new Set(val).size) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `No duplicates allowed.`,
      });
    }
  });
  const result = await Strings.safeParseAsync(["asfd", "asfd", "asfd", "asfd"]);
  expect(result.success).toEqual(false);
  if (!result.success) expect(result.error.issues.length).toEqual(2);
  Strings.parseAsync(["asfd", "qwer"]);
});
test("superRefine - type narrowing", () => {
  type NarrowType = { type: string; age: number };
  const schema = z
    .object({
      type: z.string(),
      age: z.number(),
    })
    .nullable()
    .superRefine((arg, ctx): arg is NarrowType => {
      if (!arg) {
        // still need to make a call to ctx.addIssue
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "cannot be null",
          fatal: true,
        });
        return false;
      }
      return true;
    });
  util.assertEqual<z.infer<typeof schema>, NarrowType>(true);
  expect(schema.safeParse({ type: "test", age: 0 }).success).toEqual(true);
  expect(schema.safeParse(null).success).toEqual(false);
});
test("chained mixed refining types", () => {
  type firstRefinement = { first: string; second: number; third: true };
  type secondRefinement = { first: "bob"; second: number; third: true };
  type thirdRefinement = { first: "bob"; second: 33; third: true };
  const schema = z
    .object({
      first: z.string(),
      second: z.number(),
      third: z.boolean(),
    })
    .nullable()
    .refine((arg): arg is firstRefinement => !!arg?.third)
    .superRefine((arg, ctx): arg is secondRefinement => {
      util.assertEqual<typeof arg, firstRefinement>(true);
      if (arg.first !== "bob") {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "`first` property must be `bob`",
        });
        return false;
      }
      return true;
    })
    .refine((arg): arg is thirdRefinement => {
      util.assertEqual<typeof arg, secondRefinement>(true);
      return arg.second === 33;
    });
  util.assertEqual<z.infer<typeof schema>, thirdRefinement>(true);
});
test("get inner type", () => {
  z.string()
    .refine(() => true)
    .innerType()
    .parse("asdf");
});
test("chained refinements", () => {
  const objectSchema = z
    .object({
      length: z.number(),
      size: z.number(),
    })
    .refine(({ length }) => length > 5, {
      path: ["length"],
      message: "length greater than 5",
    })
    .refine(({ size }) => size > 7, {
      path: ["size"],
      message: "size greater than 7",
    });
  const r1 = objectSchema.safeParse({
    length: 4,
    size: 9,
  });
  expect(r1.success).toEqual(false);
  if (!r1.success) expect(r1.error.issues.length).toEqual(1);
  const r2 = objectSchema.safeParse({
    length: 4,
    size: 3,
  });
  expect(r2.success).toEqual(false);
  if (!r2.success) expect(r2.error.issues.length).toEqual(2);
});
test("fatal superRefine", () => {
  const Strings = z
    .string()
    .superRefine((val, ctx) => {
      if (val === "") {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "foo",
          fatal: true,
        });
      }
    })
    .superRefine((val, ctx) => {
      if (val !== " ") {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "bar",
        });
      }
    });
  const result = Strings.safeParse("");
  expect(result.success).toEqual(false);
  if (!result.success) expect(result.error.issues.length).toEqual(1);
});
test("superRefine after skipped transform", () => {
  const schema = z
    .string()
    .regex(/^\d+$/)
    .transform((val) => Number(val))
    .superRefine((val) => {
      if (typeof val !== "number") {
        throw new Error("Called without transform");
      }
    });
  const result = schema.safeParse("");
  expect(result.success).toEqual(false);
});