Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { DateTime } from "ts-luxon";
import { rescheduledMessage, scheduledMessage } from "@/components/ui/dashboard/session-change-message";
import { Frequency } from "@/types/recurrence";
import { None, Some } from "@/types/option";

const at = (iso: string) => DateTime.fromISO(iso, { zone: "America/Chicago" });

describe("scheduledMessage", () => {
it("lists who, when and how long", () => {
const message = scheduledMessage({
personName: "Ehab Badawi",
start: at("2026-08-18T14:30"),
minutes: 50,
repeat: { kind: "none" },
otherMove: None,
});
expect(message).toBe("Scheduled · Ehab Badawi · Tuesday, August 18 · 2:30 PM · 50 min");
});

it("adds the repeat and the other session's move", () => {
const message = scheduledMessage({
personName: "Ehab Badawi",
start: at("2026-08-18T14:30"),
minutes: 50,
repeat: { kind: "repeat", frequency: Frequency.Weekly, end: { kind: "count", count: 8 } },
otherMove: Some({ personName: "Priya Raman", start: at("2026-08-19T15:30") }),
});
expect(message).toBe(
"Scheduled · Ehab Badawi · Tuesday, August 18 · 2:30 PM · 50 min · Weekly, for 8 sessions · Priya moves to Wed 19, 3:30 PM"
);
});
});

describe("rescheduledMessage", () => {
it("names the person and the new time", () => {
const message = rescheduledMessage({
personName: "Ehab Badawi",
start: at("2026-08-20T14:00"),
following: false,
otherMove: None,
});
expect(message).toBe("Rescheduled · Ehab to Thursday, August 20, 2:00 PM");
});

it("marks a series move and the other session's move", () => {
const message = rescheduledMessage({
personName: "Ehab Badawi",
start: at("2026-08-20T14:00"),
following: true,
otherMove: Some({ personName: "Alex Chen", start: at("2026-08-20T15:30") }),
});
expect(message).toBe("Rescheduled · Ehab to Thursday, August 20, 2:00 PM · and following · Alex moves to Thu 20, 3:30 PM");
});
});
76 changes: 76 additions & 0 deletions __tests__/lib/api/coaching-session-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { SessionWrite, sessionWriteFailure } from "@/lib/api/coaching-session-errors";
import { EntityApiError, PERMISSION_DENIED_MESSAGE } from "@/types/entity-api-error";

function apiError(status: number, data: unknown = {}): EntityApiError {
const axiosLike = Object.assign(new Error("Request failed"), {
isAxiosError: true,
response: { status, statusText: "Error", data },
});
return new EntityApiError("POST", "/coaching_sessions", axiosLike);
}

function networkError(): EntityApiError {
return new EntityApiError("POST", "/coaching_sessions", Object.assign(new Error("Network Error"), { isAxiosError: true }));
}

const toast = (message: string) => ({ kind: "toast", message });

describe("sessionWriteFailure", () => {
it("leaves errors that aren't API errors to the caller", () => {
expect(sessionWriteFailure(new Error("boom"), SessionWrite.Create)).toEqual({ kind: "unexpected" });
});

it("sends a revoked Google connection to Settings", () => {
expect(sessionWriteFailure(apiError(409, { error: "oauth_token_revoked" }), SessionWrite.Create)).toEqual({
kind: "reconnect",
message: "Google Meet is disconnected · Reconnect it in Settings",
});
});

it("explains an archived organization", () => {
expect(sessionWriteFailure(apiError(409, { error: "organization_archived" }), SessionWrite.Update)).toEqual(
toast("This organization is archived · It can't be changed")
);
});

it("asks to check the connection when offline", () => {
expect(sessionWriteFailure(networkError(), SessionWrite.Create)).toEqual(
toast("Couldn't create the session · Check your connection and try again")
);
expect(sessionWriteFailure(networkError(), SessionWrite.Update)).toEqual(
toast("Couldn't update the session · Check your connection and try again")
);
});

it("names a failed meeting link", () => {
expect(sessionWriteFailure(apiError(502), SessionWrite.Create)).toEqual(
toast("Couldn't create the Google Meet link · Try again")
);
});

it("asks to check the details on a validation error", () => {
expect(sessionWriteFailure(apiError(422), SessionWrite.Create)).toEqual(
toast("Couldn't create the session · Check the details and try again")
);
expect(sessionWriteFailure(apiError(422), SessionWrite.CreateSeries)).toEqual(
toast("Couldn't create the recurring sessions · Check the details and try again")
);
expect(sessionWriteFailure(apiError(422), SessionWrite.Update)).toEqual(
toast("Couldn't update the session · Check the details and try again")
);
});

it("says when permission is missing", () => {
expect(sessionWriteFailure(apiError(403), SessionWrite.Create)).toEqual(toast(PERMISSION_DENIED_MESSAGE));
});

it("falls back to a retry message", () => {
expect(sessionWriteFailure(apiError(500), SessionWrite.CreateSeries)).toEqual(
toast("Couldn't create the coaching session · Try again")
);
expect(sessionWriteFailure(apiError(500), SessionWrite.Update)).toEqual(
toast("Couldn't update the coaching session · Try again")
);
});
});
23 changes: 23 additions & 0 deletions __tests__/lib/scheduling/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DateTime } from "ts-luxon";
import type { BusySession } from "@/lib/scheduling";

export const ZONE = "America/Chicago";

export const at = (iso: string) => DateTime.fromISO(iso, { zone: ZONE });

export const busy = (id: string, personName: string, start: string, end: string): BusySession => ({
id,
personName,
start: at(start),
end: at(end),
});

/** Monday, the day before the fixtures' Tuesday. */
export const NOW = at("2026-08-17T10:00");
export const TUESDAY = at("2026-08-18T00:00");

export const EHAB = busy("s-ehab", "Ehab Badawi", "2026-08-18T14:30", "2026-08-18T15:20");
export const PRIYA = busy("s-priya", "Priya Raman", "2026-08-18T16:30", "2026-08-18T17:20");
export const ALEX_WED = busy("s-alex", "Alex Chen", "2026-08-19T09:00", "2026-08-19T09:50");

export const hhmm = (times: DateTime[]) => times.map((t) => t.toFormat("HH:mm"));
135 changes: 135 additions & 0 deletions __tests__/lib/scheduling/scheduling-copy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, expect, it } from "vitest";
import {
busyHeader,
busyRange,
createsLine,
currentlyLine,
meetingLine,
moveSubmitLabel,
nothingElseLine,
overlapLine,
repeatLabel,
resolveSubline,
scheduleSubmitLabel,
insteadLabel,
usualSlotLabel,
whoSubline,
} from "@/lib/scheduling-copy";
import { Frequency } from "@/types/recurrence";
import { Provider } from "@/types/provider";
import { None, Some } from "@/types/option";
import { EHAB, NOW, TUESDAY, at, busy } from "./fixtures";

describe("usualSlotLabel", () => {
it("names the weekday in the plural and the time", () => {
expect(usualSlotLabel({ weekday: 2, minute: 870 })).toBe("Usually Tuesdays, 2:30 PM");
expect(usualSlotLabel({ weekday: 5, minute: 540 })).toBe("Usually Fridays, 9:00 AM");
});
});

describe("whoSubline", () => {
it("shows the usual slot first", () => {
const usual = Some({ weekday: 2, minute: 870 });
expect(whoSubline({ usual, next: None, last: Some(at("2026-08-11T14:30")) }, NOW)).toBe(
"Usually Tuesdays, 2:30 PM"
);
});

it("names a first session by weekday within the week", () => {
expect(whoSubline({ usual: None, next: Some(at("2026-08-20T10:00")), last: None }, NOW)).toBe(
"First session Thursday"
);
});

it("names a later first session by date", () => {
expect(whoSubline({ usual: None, next: Some(at("2026-09-03T10:00")), last: None }, NOW)).toBe(
"First session Sep 3"
);
});

it("falls back to the last session", () => {
const line = whoSubline({ usual: None, next: Some(at("2026-08-20T10:00")), last: Some(at("2026-06-12T10:00")) }, NOW);
expect(line).toBe("Last session Jun 12");
});

it("says when there are no sessions", () => {
expect(whoSubline({ usual: None, next: None, last: None }, NOW)).toBe("No sessions yet");
});
});

describe("When step lines", () => {
it("heads the busy list with the long date", () => {
expect(busyHeader(TUESDAY)).toBe("Already on Tuesday, August 18");
expect(nothingElseLine(TUESDAY)).toBe("Nothing else on Tuesday, August 18.");
});

it("shows a busy range with both meridiems", () => {
expect(busyRange(EHAB)).toBe("2:30 PM–3:20 PM");
});

it("names the clash by first name with a compact range", () => {
expect(overlapLine(EHAB)).toBe("Overlaps with Ehab, 2:30–3:20 PM");
});

it("keeps both meridiems when the clash crosses noon", () => {
const noon = busy("s-noon", "Sarah Kim", "2026-08-18T11:30", "2026-08-18T12:20");
expect(overlapLine(noon)).toBe("Overlaps with Sarah, 11:30 AM–12:20 PM");
});

it("offers the free time", () => {
expect(insteadLabel(at("2026-08-18T15:30"))).toBe("Use 3:30 PM instead");
});
});

describe("Repeat labels", () => {
it("describes each choice", () => {
expect(repeatLabel({ kind: "none" })).toBe("Doesn't repeat");
expect(repeatLabel({ kind: "repeat", frequency: Frequency.Weekly, end: { kind: "count", count: 8 } })).toBe(
"Weekly, for 8 sessions"
);
expect(repeatLabel({ kind: "repeat", frequency: Frequency.Biweekly, end: { kind: "until", until: "2026-12-15" } })).toBe(
"Every 2 weeks, until Dec 15"
);
expect(repeatLabel({ kind: "repeat", frequency: Frequency.Monthly, end: { kind: "count", count: 1 } })).toBe(
"Monthly, for 1 session"
);
});
});

describe("submit labels", () => {
it("counts what scheduling creates", () => {
expect(scheduleSubmitLabel({ kind: "none" })).toBe("Schedule session");
expect(scheduleSubmitLabel({ kind: "repeat", frequency: Frequency.Weekly, end: { kind: "count", count: 8 } })).toBe(
"Schedule 8 sessions"
);
expect(scheduleSubmitLabel({ kind: "repeat", frequency: Frequency.Weekly, end: { kind: "until", until: "2026-12-15" } })).toBe(
"Schedule series"
);
});

it("names the move scope", () => {
expect(moveSubmitLabel(false)).toBe("Reschedule session");
expect(moveSubmitLabel(true)).toBe("Reschedule this and following");
});
});

describe("rail and resolve lines", () => {
it("summarizes what scheduling creates", () => {
expect(createsLine(1, "Ehab")).toBe("1 session with Ehab");
expect(createsLine(8, "Ehab")).toBe("8 sessions with Ehab");
});

it("names the meeting link by provider, or nothing", () => {
expect(meetingLine(Some(Provider.Google))).toEqual(Some("Google Meet link included"));
expect(meetingLine(Some(Provider.Zoom))).toEqual(Some("Zoom link included"));
expect(meetingLine(None)).toEqual(None);
});

it("says where the moved session is now", () => {
expect(currentlyLine(at("2026-08-18T14:30"))).toBe("Currently Tue, Aug 18, 2:30 PM");
});

it("explains which session has to move", () => {
expect(resolveSubline(EHAB)).toBe("Ehab's session on Tue 18 at 2:30 PM overlaps the new one. Move it to:");
});
});
Loading
Loading