import { describe, expect, it } from "vitest"; import { closingNote, focusCard, focusFactValue, intakeContext, intakeStep, nextIntakeStep, readIntakeAnswer, secondaryOption, } from "./onboarding-intake.js"; // Intl may separate the time from AM/PM with a narrow no-break space. const plain = (text: string) => text.replace(/\s/g, " "); const off = { morning: null, evening: null, weekly: null }; describe("onboarding steps", () => { it("writes focus facts in the user's locale", () => { expect(focusFactValue("day", "en-US")).toBe("Wants help with day-to-day work first"); expect(focusFactValue("day", "de-DE")).toBe( "Möchte zuerst bei der täglichen Arbeit unterstützt werden", ); }); it("asks in order and offers notifications only to a device that can still turn push on", () => { expect(nextIntakeStep("focus", { canEnablePush: false })?.id).toBe("goal"); expect(nextIntakeStep("goal", { canEnablePush: false })?.id).toBe("rhythm"); expect(nextIntakeStep("rhythm", { canEnablePush: false })?.id).toBe("quiet_hours"); expect(nextIntakeStep("quiet_hours", { canEnablePush: true })?.id).toBe("notifications"); expect(nextIntakeStep("quiet_hours", { canEnablePush: false })).toBeNull(); expect(nextIntakeStep("notifications", { canEnablePush: true })).toBeNull(); }); it("gives every onboarding card a quiet way out, and another bot's focus card none", () => { const context = intakeContext(null, new Date()); for (const id of ["focus", "goal", "rhythm", "quiet_hours", "notifications"] as const) { const card = intakeStep(id).card(context); expect(card.step).toBe(id); expect(secondaryOption(card)).toBeDefined(); } expect(focusCard().step).toBeUndefined(); expect(secondaryOption(focusCard())).toBeUndefined(); }); it("writes quiet hours in the user's locale", () => { const labels = (locale: string | null) => intakeStep("quiet_hours") .card(intakeContext({ locale, timeZone: null }, new Date())) .options.map((option) => plain(option.label)); expect(labels("de-AT")).toEqual(["22:00 – 7:00", "23:00 – 8:00", "Skip"]); expect(labels("en-US")).toEqual(["10:00 PM – 7:00 AM", "11:00 PM – 8:00 AM", "Skip"]); expect(labels("not a tag")).toEqual(labels("en")); }); }); describe("readIntakeAnswer", () => { const goal = intakeStep("goal").card(intakeContext(null, new Date())); const rhythm = intakeStep("rhythm").card(intakeContext(null, new Date())); it("accepts the card's own options and words only where the card takes them", () => { expect(readIntakeAnswer(rhythm, "weekly", undefined)).toEqual({ option: { id: "weekly", letter: "C", label: "Once a week" }, }); expect(readIntakeAnswer(rhythm, "skip", undefined)).toBeNull(); expect(readIntakeAnswer(rhythm, "_text", "Mornings")).toBeNull(); expect(readIntakeAnswer(goal, "_text", " Run a 10K ")).toEqual({ text: "Run a 10K" }); expect(readIntakeAnswer(goal, "_text", " ")).toBeNull(); expect(readIntakeAnswer(goal, "skip", undefined)?.option?.label).toBe("Skip"); }); }); describe("closingNote", () => { const vienna = (iso: string, locale = "de-AT") => intakeContext({ locale, timeZone: "Europe/Vienna" }, new Date(iso)); const note = (rhythm: Parameters[0], context: ReturnType) => { const { nextCheckInAt, text } = closingNote(rhythm, context); return { nextCheckInAt, text: plain(text) }; }; it("names the first check-in in the user's time zone", () => { const morning = { ...off, morning: "08:00" }; expect(note(morning, vienna("2026-10-07T04:00:00Z"))).toEqual({ nextCheckInAt: "2026-10-07T06:00:00.000Z", text: "All set. I’ll check in today at 8:00.", }); expect(note(morning, vienna("2026-10-07T09:00:00Z"))).toEqual({ nextCheckInAt: "2026-10-08T06:00:00.000Z", text: "All set. I’ll check in tomorrow at 8:00.", }); expect(note({ ...morning, evening: "18:00" }, vienna("2026-10-07T09:00:00Z")).text).toBe( "All set. I’ll check in today at 18:00.", ); // Wednesday in Vienna; the weekly review is on Sunday. const weekly = { ...off, weekly: { day: 0, time: "18:00" } }; expect(note(weekly, vienna("2026-10-07T09:00:00Z", "en-US"))).toEqual({ nextCheckInAt: "2026-10-11T16:00:00.000Z", text: "All set. I’ll check in on Sunday at 6:00 PM.", }); }); it("says the user can just write when no check-in is set", () => { expect(closingNote(off, vienna("2026-10-07T09:00:00Z"))).toEqual({ kind: "intake_note", note: "done", nextCheckInAt: null, text: "All set. I’m here whenever you need me.", }); }); it("falls back to English and UTC for an unusable profile", () => { expect(intakeContext({ locale: "??", timeZone: "Mars/Olympus" }, new Date(0))).toMatchObject({ locale: "en", timeZone: "UTC", }); }); });