import { ClientSchedule, ClientScheduleEventSlot, ClientScheduleLocation, toIso } from "./client-schedule"; import { describe, expect, test } from "vitest"; import type { ApiSchedule } from "~/shared/types/api"; import type { Living } from "~/shared/types/common"; import { DateTime, FixedOffsetZone } from "~/shared/utils/luxon"; const locale = "en-GB"; const now = DateTime.now().setLocale(locale); const zone = now.zone; const nowIso = now.setZone(FixedOffsetZone.utcInstance).toISO(); function fixtureClientSchedule() { const left = new ClientScheduleLocation(1, now, false, "Left", ""); const right = new ClientScheduleLocation(2, now, false, "Right", "This is the right place"); const events = [ new ClientScheduleEvent( 1, now, false, "Up", false, "", false, "What's Up?", 0, [new ClientScheduleEventSlot(1, now, now.plus({ hours: 1 }), [left], new Set(), 0)], ), new ClientScheduleEvent( 2, now, false, "Down", false, "", false, "", 0, [new ClientScheduleEventSlot(2, now, now.plus({ hours: 2 }), [right], new Set(), 0)], ), ]; const red = new ClientScheduleRole(1, now, false, "Red", "Is a color."); const blue = new ClientScheduleRole(2, now, false, "Blue", ""); const shifts = [ new ClientScheduleShift( 1, now, false, red, "White", "", [new ClientScheduleShiftSlot(1, now, now.plus({ hours: 1 }), new Set())], ), new ClientScheduleShift( 2, now, false, blue, "Black", "Is dark.", [new ClientScheduleShiftSlot(2, now, now.plus({ hours: 2 }), new Set())], ), ]; return new ClientSchedule( 111, now, false, new Map([ [left.id, left], [right.id, right], ]), new Map(events.map(event => [event.id, event])), new Map([ [red.id, red], [blue.id, blue], ]), new Map(shifts.map(shift => [shift.id, shift])), ); } function fixtureApiSchedule(): Living { return { id: 111, updatedAt: nowIso, locations: [ { id: 1, updatedAt: nowIso, name: "Left", }, { id: 2, updatedAt: nowIso, name: "Right", description: "This is the right place", }, ], events: [ { id: 1, updatedAt: nowIso, name: "Up", description: "What's Up?", slots: [{ id: 1, start: nowIso, end: toIso(now.plus({ hours: 1 })), locationIds: [1], }], }, { id: 2, updatedAt: nowIso, name: "Down", slots: [{ id: 2, start: nowIso, end: toIso(now.plus({ hours: 2 })), locationIds: [2], }], }, ], roles: [ { id: 1, updatedAt: nowIso, name: "Red", description: "Is a color.", }, { id: 2, updatedAt: nowIso, name: "Blue", }, ], shifts: [ { id: 1, updatedAt: nowIso, name: "White", roleId: 1, slots: [{ id: 1, start: nowIso, end: toIso(now.plus({ hours: 1 })), }], }, { id: 2, updatedAt: nowIso, name: "Black", description: "Is dark.", roleId: 2, slots: [{ id: 2, start: nowIso, end: toIso(now.plus({ hours: 2 })), }], }, ], }; } describe("class ClientSchedule", () => { test("load from api", () => { const schedule = ClientSchedule.fromApi(fixtureApiSchedule(), { zone, locale }) expect(schedule).toStrictEqual(fixtureClientSchedule()); }); test("save to api", () => { const schedule = fixtureClientSchedule(); expect(schedule.toApi(false)).toEqual(fixtureApiSchedule()) }); const updatePatterns = [ "aa a aa", "ba a aa", "-a a aa", "ab a ab", "bb a aa", "-b a ab", "ax a ax", "bx a ax", "-- a aa", "-x a ax", "aa x --", "ba x -a", "-a x -a", "ab x -b", "bb x --", "-b x -b", "ax x --", "bx x --", "-x x --", "-- x --", ]; for (const pattern of updatePatterns) { test(`apply diff pattern ${pattern}`, () => { const fixture: Record = { a: new ClientScheduleLocation(1, now, false, "A", ""), b: new ClientScheduleLocation(1, now, false, "B", ""), x: new ClientScheduleLocation(1, now, true, "X", ""), }; const schedule = new ClientSchedule(111, now, false, new Map(), new Map(), new Map(), new Map()); if (fixture[pattern[0]]) schedule.originalLocations.set(1, fixture[pattern[0]]); if (fixture[pattern[1]]) schedule.locations.set(1, fixture[pattern[1]]); const update = fixture[pattern[3]]; const expectedOriginalLocation = pattern[5] === "x" ? undefined : fixture[pattern[5]]; const expectedLocation = fixture[pattern[6]]; schedule.applyUpdate({ id: 111, updatedAt: nowIso, locations: [update.toApi()], }, { zone, locale }); expect(schedule.originalLocations.get(1)).toEqual(expectedOriginalLocation); expect(schedule.locations.get(1)).toEqual(expectedLocation); if (pattern.slice(5) === "aa") expect(schedule.originalLocations.get(1)).toBe(schedule.locations.get(1)); }); } test("create location", () => { const schedule = fixtureClientSchedule(); const location = new ClientScheduleLocation(3, now, false, "New location", ""); schedule.setLocation(location); expect(schedule.originalLocations.get(3)).toBe(undefined); expect(schedule.locations.get(3)).toBe(location); }); test("update location", () => { const schedule = fixtureClientSchedule(); const original = schedule.locations.get(1)!; const copy = original.clone(); copy.name = "Modified Location"; schedule.setLocation(copy); expect(schedule.originalLocations.get(1)).toBe(original); expect(schedule.locations.get(1)).toBe(copy); expect(schedule.events.get(1)!.slots[0].locations[0]).toBe(copy); }); test("delete location in use throws", () => { const schedule = fixtureClientSchedule(); const original = schedule.locations.get(1)!; const copy = original.clone(); copy.deleted = true; expect( () => { schedule.setLocation(copy); } ).toThrow(new Error('Cannot delete location, event "Up" depends on it')); }); test("delete location", () => { const schedule = fixtureClientSchedule(); const event = schedule.events.get(1)!.clone(); event.slots = []; schedule.setEvent(event); const original = schedule.locations.get(1)!; const copy = original.clone(); copy.deleted = true; schedule.setLocation(copy); expect(schedule.originalLocations.get(1)).toBe(original); expect(schedule.locations.get(1)).toBe(copy); }); test("create role", () => { const schedule = fixtureClientSchedule(); const role = new ClientScheduleRole(3, now, false, "New role", ""); schedule.setRole(role); expect(schedule.originalRoles.get(3)).toBe(undefined); expect(schedule.roles.get(3)).toBe(role); }); test("update role", () => { const schedule = fixtureClientSchedule(); const original = schedule.roles.get(1)!; const copy = original.clone(); copy.name = "Modified Role"; schedule.setRole(copy); expect(schedule.originalRoles.get(1)).toBe(original); expect(schedule.roles.get(1)).toBe(copy); expect(schedule.shifts.get(1)!.role).toBe(copy); }); test("delete role in use throws", () => { const schedule = fixtureClientSchedule(); const original = schedule.roles.get(1)!; const copy = original.clone(); copy.deleted = true; expect( () => { schedule.setRole(copy); } ).toThrow(new Error('Cannot delete role, shift "White" depends on it')); }); test("delete role", () => { const schedule = fixtureClientSchedule(); const shift = schedule.shifts.get(1)!.clone(); shift.role = schedule.roles.get(2)!; schedule.setShift(shift); const original = schedule.roles.get(1)!; const copy = original.clone(); copy.deleted = true; schedule.setRole(copy); expect(schedule.originalRoles.get(1)).toBe(original); expect(schedule.roles.get(1)).toBe(copy); }); });