93 lines
4.2 KiB
TypeScript
93 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|||
|
|
import { issuesToMap, planVisitSchema, todayIsoDate, type PlanVisitInput } from "@/lib/schema";
|
||
|
|
import { formatNanpDisplay, normalizeNanpPhone, smsLink } from "@/lib/phone";
|
||
|
|
|
||
|
|
const base: PlanVisitInput = {
|
||
|
|
submissionId: "6f1a2c3e-4b5d-4e6f-8a9b-0c1d2e3f4a5b",
|
||
|
|
firstName: "Jordan",
|
||
|
|
lastName: "",
|
||
|
|
email: "jordan@example.com",
|
||
|
|
phone: "",
|
||
|
|
campusId: "norwood",
|
||
|
|
visitDate: "",
|
||
|
|
serviceTime: "",
|
||
|
|
questions: "",
|
||
|
|
emailConsent: false,
|
||
|
|
smsConsent: false,
|
||
|
|
consentVersion: "2026-09-22.v1",
|
||
|
|
website: "",
|
||
|
|
startedAt: Date.now() - 10_000,
|
||
|
|
attribution: {},
|
||
|
|
};
|
||
|
|
|
||
|
|
describe("planVisitSchema", () => {
|
||
|
|
it("accepts a minimal valid submission", () => {
|
||
|
|
expect(planVisitSchema.safeParse(base).success).toBe(true);
|
||
|
|
});
|
||
|
|
it("requires a first name and a real email", () => {
|
||
|
|
const r = planVisitSchema.safeParse({ ...base, firstName: " ", email: "nope" });
|
||
|
|
expect(r.success).toBe(false);
|
||
|
|
const errs = issuesToMap(r.success ? [] : r.error.issues);
|
||
|
|
expect(errs.firstName).toBeTruthy();
|
||
|
|
expect(errs.email).toBeTruthy();
|
||
|
|
});
|
||
|
|
it("rejects unknown or launching campuses", () => {
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, campusId: "orlando" }).success).toBe(false);
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, campusId: "mars" }).success).toBe(false);
|
||
|
|
});
|
||
|
|
it("enforces consent rules: SMS consent requires a valid mobile number", () => {
|
||
|
|
const noPhone = planVisitSchema.safeParse({ ...base, smsConsent: true, phone: "" });
|
||
|
|
expect(noPhone.success).toBe(false);
|
||
|
|
expect(issuesToMap(noPhone.success ? [] : noPhone.error.issues).phone).toMatch(/mobile number/i);
|
||
|
|
|
||
|
|
const badPhone = planVisitSchema.safeParse({ ...base, smsConsent: true, phone: "123" });
|
||
|
|
expect(badPhone.success).toBe(false);
|
||
|
|
|
||
|
|
const good = planVisitSchema.safeParse({ ...base, smsConsent: true, phone: "(508) 555-0142" });
|
||
|
|
expect(good.success).toBe(true);
|
||
|
|
});
|
||
|
|
it("does not bundle consents: email consent alone never needs a phone", () => {
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, emailConsent: true }).success).toBe(true);
|
||
|
|
});
|
||
|
|
it("consent boxes default to false in the initial state", () => {
|
||
|
|
expect(base.emailConsent).toBe(false);
|
||
|
|
expect(base.smsConsent).toBe(false);
|
||
|
|
});
|
||
|
|
it("rejects past visit dates and accepts today", () => {
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, visitDate: "2020-01-01" }).success).toBe(false);
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, visitDate: todayIsoDate() }).success).toBe(true);
|
||
|
|
});
|
||
|
|
it("rejects a filled honeypot", () => {
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, website: "http://spam.example" }).success).toBe(false);
|
||
|
|
});
|
||
|
|
it("caps children and validates age ranges", () => {
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, children: 3, childAgeRanges: ["0-2", "6-10"] }).success).toBe(true);
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, children: 99 }).success).toBe(false);
|
||
|
|
expect(planVisitSchema.safeParse({ ...base, childAgeRanges: ["30-40"] }).success).toBe(false);
|
||
|
|
});
|
||
|
|
it("trims free text", () => {
|
||
|
|
const r = planVisitSchema.safeParse({ ...base, firstName: " Sam ", questions: " Is parking free? " });
|
||
|
|
expect(r.success && r.data.firstName).toBe("Sam");
|
||
|
|
expect(r.success && r.data.questions).toBe("Is parking free?");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("phone helpers", () => {
|
||
|
|
it("normalizes NANP numbers to E.164", () => {
|
||
|
|
expect(normalizeNanpPhone("(508) 695-1300")).toBe("+15086951300");
|
||
|
|
expect(normalizeNanpPhone("1-508-695-1300")).toBe("+15086951300");
|
||
|
|
expect(normalizeNanpPhone("+1 508 695 1300")).toBe("+15086951300");
|
||
|
|
expect(normalizeNanpPhone("508-695-130")).toBeNull();
|
||
|
|
expect(normalizeNanpPhone("011-555-1234")).toBeNull(); // area code starting with 0
|
||
|
|
expect(normalizeNanpPhone("911-555-1234")).toBeNull(); // N11
|
||
|
|
});
|
||
|
|
it("formats for display", () => {
|
||
|
|
expect(formatNanpDisplay("+15086951300")).toBe("(508) 695-1300");
|
||
|
|
});
|
||
|
|
it("builds an sms: link with an editable, encoded body", () => {
|
||
|
|
const link = smsLink("+15086951300", "Hi! I'm thinking about visiting.");
|
||
|
|
expect(link.startsWith("sms:+15086951300?&body=")).toBe(true);
|
||
|
|
expect(decodeURIComponent(link.split("body=")[1])).toBe("Hi! I'm thinking about visiting.");
|
||
|
|
});
|
||
|
|
});
|