Files
visit.waterschurch.org/tests/schema.test.ts
T
Waters ChurchandClaude Fable 5.1 78cd0cc550 Plan Your Visit landing page for visit.waterschurch.org
Next.js 16 static export + PHP 8 API for cPanel/Apache hosting.

- Location-aware campus finder (IP estimate, optional precise location, ZIP search)
- Three-step Plan Your Visit form with separate email/SMS consent
- PHP API: validation, anti-spam, rate limiting, SQLite lead store,
  Mailchimp and Sinch Engage adapters, idempotent retries via cron
- Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and
  consent-aware analytics hooks
- Vitest and PHP test suites, cPanel deployment script and docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 13:22:26 -04:00

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.");
});
});