owltide/utils/client-schedule.nuxt.test.ts

488 lines
17 KiB
TypeScript
Raw Normal View History

import { ClientEntity, ClientSchedule, ClientScheduleEventSlot, ClientScheduleLocation, ClientScheduleShiftSlot, toIso } from "~/utils/client-schedule";
import { describe, expect, test } from "vitest";
import type { ApiSchedule } from "~/shared/types/api";
import type { Id, Living } from "~/shared/types/common";
import { DateTime, FixedOffsetZone } from "~/shared/utils/luxon";
const locale = "en-GB";
const now = DateTime.now().setLocale(locale);
const later = now.plus({ minutes: 2 });
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,
idMap([new ClientScheduleEventSlot(1, false, 1, now, now.plus({ hours: 1 }), [left], new Set(), 0)]),
),
new ClientScheduleEvent(
2, now, false, "Down", false, "", false, "", 0,
idMap([new ClientScheduleEventSlot(2, false, 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", "",
idMap([new ClientScheduleShiftSlot(1, false, 1, now, now.plus({ hours: 1 }), new Set())]),
),
new ClientScheduleShift(
2, now, false, blue, "Black", "Is dark.",
idMap([new ClientScheduleShiftSlot(2, false, 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<ApiSchedule> {
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 fixtureClient: Record<string, ClientScheduleLocation> = {
a: new ClientScheduleLocation(1, now, false, "A", ""),
b: new ClientScheduleLocation(1, now, false, "B", ""),
x: new ClientScheduleLocation(1, now, true, "X", ""),
};
const fixtureServer: Record<string, ClientScheduleLocation> = {
a: new ClientScheduleLocation(1, later, false, "A", ""),
b: new ClientScheduleLocation(1, later, false, "B", ""),
x: new ClientScheduleLocation(1, later, true, "X", ""),
};
const schedule = new ClientSchedule(111, now, false, new Map(), new Map(), new Map(), new Map());
if (fixtureClient[pattern[0]])
schedule.originalLocations.set(1, fixtureClient[pattern[0]]);
if (fixtureClient[pattern[1]])
schedule.locations.set(1, fixtureClient[pattern[1]]);
const update = fixtureServer[pattern[3]];
const expectedOriginalLocation = pattern[5] === "x" ? undefined : fixtureServer[pattern[5]];
const expectedLocation = pattern[5] === pattern[6] ? fixtureServer[pattern[6]] : fixtureClient[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));
});
}
const entityTests: [string, (schedule: ClientSchedule) => ClientEntity][] = [
[
"location",
() => new ClientScheduleLocation(3, now, false, "New location", "")
],
[
"event",
() => new ClientScheduleEvent(3, now, false, "New location", false, "", false, "", 0, new Map())
],
[
"role",
() => new ClientScheduleRole(3, now, false, "New location", "")
],
[
"shift",
(schedule) => new ClientScheduleShift(3, now, false, schedule.roles.get(1)!, "New location", "", new Map())
],
] as const;
for (const [name, create] of entityTests) {
describe(name, () => {
const Name = name[0].toUpperCase() + name.slice(1);
test(`create`, () => {
const schedule = fixtureClientSchedule();
const entity = create(schedule);
expect(schedule.modified).toBe(false);
// Create
(schedule as any)[`set${Name}`](entity);
// Check
expect(schedule.modified).toBe(true);
expect((schedule as any)[`isModified${Name}`](entity.id)).toBe(true);
expect((schedule as any)[`original${Name}s`].get(entity.id)).toBe(undefined);
expect((schedule as any)[`${name}s`].get(entity.id)).toBe(entity);
});
test("edit", () => {
const schedule = fixtureClientSchedule();
const original = (schedule as any)[`${name}s`].get(1);
expect(schedule.modified).toBe(false);
expect((schedule as any)[`isModified${Name}`](1)).toBe(false);
// Edit
(schedule as any)[`edit${Name}`](original, { name: `Modified ${name}` })
// Check
expect(schedule.modified).toBe(true);
expect((schedule as any)[`isModified${Name}`](1)).toBe(true);
expect((schedule as any)[`original${Name}s`].get(1)).toBe(original);
if (name === "location") {
expect(schedule.events.get(1)!.slots.get(1)!.locations[0]).toBe(schedule.locations.get(1));
} else if (name === "role") {
expect(schedule.shifts.get(1)!.role).toBe(schedule.roles.get(1));
}
});
if (name === "location") {
test("delete location in use throws", () => {
const schedule = fixtureClientSchedule();
expect(
() => { schedule.editLocation(schedule.locations.get(1)!, { deleted: true }); }
).toThrow(new Error('Cannot delete location, event "Up" depends on it'));
});
} else if (name === "role") {
test("delete role in use throws", () => {
const schedule = fixtureClientSchedule();
expect(
() => { schedule.editRole(schedule.roles.get(1)!, { deleted: true }); }
).toThrow(new Error('Cannot delete role, shift "White" depends on it'));
});
}
test("delete", () => {
const schedule = fixtureClientSchedule();
const original = (schedule as any)[`${name}s`].get(1);
expect(schedule.modified).toBe(false);
expect((schedule as any)[`isModified${Name}`](1)).toBe(false);
// Delete
if (name === "location") {
schedule.editEvent(schedule.events.get(1)!, { deleted: true });
} else if (name === "role") {
schedule.editShift(schedule.shifts.get(1)!, { deleted: true });
}
(schedule as any)[`edit${Name}`](original, { deleted: true })
// Check
expect(schedule.modified).toBe(true);
expect((schedule as any)[`isModified${Name}`](1)).toBe(true);
expect((schedule as any)[`original${Name}s`].get(1)).toBe(original);
expect((schedule as any)[`${name}s`].get(1).deleted).toBe(true);
});
});
}
function validateSlotRelations(
slots: Map<Id, ClientScheduleShiftSlot> | Map<Id, ClientScheduleEventSlot>,
entites: Map<Id, ClientScheduleShift> | Map<Id, ClientScheduleEvent>,
prefix: string,
) {
const remainingIds = new Set(slots.keys())
for (const shift of entites.values()) {
for (const slot of shift.slots.values()) {
if (!slots.has(slot.id)) {
throw Error(`${prefix}: shift ${shift.name}:${shift.id} has slot ${slot.id} that is not in shiftSlots`);
}
if (slots.get(slot.id) !== slot) {
throw Error(`${prefix}: shift ${shift.name}:${shift.id} has slot ${slot.id} which does not match the corresponding slot in shiftSlots.`);
}
if (!remainingIds.has(slot.id)) {
throw Error(`${prefix}: shift ${shift.name}:${shift.id} has slot ${slot.id} that has been seen twice.`);
}
remainingIds.delete(slot.id);
}
}
if (remainingIds.size) {
throw Error(`${prefix}: shiftSlots ${[...remainingIds].join(", ")} does not have a corresponding shift`);
}
}
describe("event slot", () => {
test("edit", () => {
const schedule = fixtureClientSchedule();
expect(schedule.modified).toBe(false);
expect(schedule.isModifiedEvent(1)).toBe(false);
expect(schedule.isModifiedEvent(2)).toBe(false);
expect(schedule.isModifiedEventSlot(1)).toBe(false);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
// Modify
schedule.editEventSlot(schedule.eventSlots.get(1)!, { start: later });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedEvent(1)).toBe(true);
expect(schedule.isModifiedEvent(2)).toBe(false);
expect(schedule.isModifiedEventSlot(1)).toBe(true);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.eventSlots, schedule.events, "current");
validateSlotRelations(schedule.eventSlots, schedule.events, "original");
});
test("move to another event", () => {
const schedule = fixtureClientSchedule();
expect(schedule.modified).toBe(false);
expect(schedule.isModifiedEvent(1)).toBe(false);
expect(schedule.isModifiedEvent(2)).toBe(false);
expect(schedule.isModifiedEventSlot(1)).toBe(false);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
// Modify
schedule.editEventSlot(schedule.eventSlots.get(1)!, { eventId: 2 });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedEvent(1)).toBe(true);
expect(schedule.isModifiedEvent(2)).toBe(true);
expect(schedule.isModifiedEventSlot(1)).toBe(true);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(0);
expect(schedule.events.get(2)!.slots.size).toBe(2);
validateSlotRelations(schedule.eventSlots, schedule.events, "current");
validateSlotRelations(schedule.eventSlots, schedule.events, "original");
// Move back
schedule.editEventSlot(schedule.eventSlots.get(1)!, { eventId: 1 });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedEvent(1)).toBe(true);
expect(schedule.isModifiedEvent(2)).toBe(true);
expect(schedule.isModifiedEventSlot(1)).toBe(true);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.eventSlots, schedule.events, "current");
validateSlotRelations(schedule.eventSlots, schedule.events, "original");
});
test("restore", () => {
const schedule = fixtureClientSchedule();
schedule.editEventSlot(schedule.eventSlots.get(1)!, { start: later });
schedule.restoreEventSlot(1);
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedEvent(1)).toBe(true);
expect(schedule.isModifiedEvent(2)).toBe(false);
expect(schedule.isModifiedEventSlot(1)).toBe(false);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.eventSlots, schedule.events, "current");
validateSlotRelations(schedule.eventSlots, schedule.events, "original");
});
test("restore from another event", () => {
const schedule = fixtureClientSchedule();
schedule.editEventSlot(schedule.eventSlots.get(1)!, { eventId: 2 });
schedule.restoreEventSlot(1);
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedEvent(1)).toBe(true);
expect(schedule.isModifiedEvent(2)).toBe(true);
expect(schedule.isModifiedEventSlot(1)).toBe(false);
expect(schedule.isModifiedEventSlot(2)).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.eventSlots, schedule.events, "current");
validateSlotRelations(schedule.eventSlots, schedule.events, "original");
});
});
describe("shift slot", () => {
test("edit", () => {
const schedule = fixtureClientSchedule();
expect(schedule.modified).toBe(false);
expect(schedule.isModifiedShift(1)).toBe(false);
expect(schedule.isModifiedShift(2)).toBe(false);
expect(schedule.isModifiedShiftSlot(1)).toBe(false);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
// Modify
schedule.editShiftSlot(schedule.shiftSlots.get(1)!, { start: later });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedShift(1)).toBe(true);
expect(schedule.isModifiedShift(2)).toBe(false);
expect(schedule.isModifiedShiftSlot(1)).toBe(true);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "current");
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "original");
});
test("move to another shift", () => {
const schedule = fixtureClientSchedule();
expect(schedule.modified).toBe(false);
expect(schedule.isModifiedShift(1)).toBe(false);
expect(schedule.isModifiedShift(2)).toBe(false);
expect(schedule.isModifiedShiftSlot(1)).toBe(false);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
// Modify
schedule.editShiftSlot(schedule.shiftSlots.get(1)!, { shiftId: 2 });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedShift(1)).toBe(true);
expect(schedule.isModifiedShift(2)).toBe(true);
expect(schedule.isModifiedShiftSlot(1)).toBe(true);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(0);
expect(schedule.shifts.get(2)!.slots.size).toBe(2);
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "current");
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "original");
// Move back
schedule.editShiftSlot(schedule.shiftSlots.get(1)!, { shiftId: 1 });
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedShift(1)).toBe(true);
expect(schedule.isModifiedShift(2)).toBe(true);
expect(schedule.isModifiedShiftSlot(1)).toBe(true);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "current");
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "original");
});
test("restore", () => {
const schedule = fixtureClientSchedule();
schedule.editShiftSlot(schedule.shiftSlots.get(1)!, { start: later });
schedule.restoreShiftSlot(1);
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedShift(1)).toBe(true);
expect(schedule.isModifiedShift(2)).toBe(false);
expect(schedule.isModifiedShiftSlot(1)).toBe(false);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "current");
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "original");
});
test("restore from another shift", () => {
const schedule = fixtureClientSchedule();
schedule.editShiftSlot(schedule.shiftSlots.get(1)!, { shiftId: 2 });
schedule.restoreShiftSlot(1);
// Check
expect(schedule.modified).toBe(true);
expect(schedule.isModifiedShift(1)).toBe(true);
expect(schedule.isModifiedShift(2)).toBe(true);
expect(schedule.isModifiedShiftSlot(1)).toBe(false);
expect(schedule.isModifiedShiftSlot(2)).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "current");
validateSlotRelations(schedule.shiftSlots, schedule.shifts, "original");
});
});
});