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

630 lines
20 KiB
TypeScript
Raw Normal View History

/*
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { ClientEntity } from "~/utils/client-entity";
import { ClientSchedule, ClientScheduleEventSlot, ClientScheduleLocation, ClientScheduleShiftSlot, toIso } from "~/utils/client-schedule";
import { describe, expect, test } from "vitest";
import type { ApiEntity, ApiSchedule } from "~/shared/types/api";
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();
const laterIso = later.setZone(FixedOffsetZone.utcInstance).toISO();
function fixtureClientSchedule(multiSlot = false) {
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 Set(multiSlot ? [1, 2] : [1]),
),
new ClientScheduleEvent(
2, now, false, "Down", false, "", false, "", 0, new Set(multiSlot ? [] : [2]),
),
];
const eventSlots = idMap([
new ClientScheduleEventSlot(1, false, false, 1, now, now.plus({ hours: 1 }), new Set([left.id]), new Set(), 0),
new ClientScheduleEventSlot(2, false, false, multiSlot ? 1 : 2, now, now.plus({ hours: 2 }), new Set([right.id]), 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.id, "White", "", new Set(multiSlot ? [1, 2] : [1]),
),
new ClientScheduleShift(
2, now, false, blue.id, "Black", "Is dark.", new Set(multiSlot ? [] : [2]),
),
];
const shiftSlots = idMap([
new ClientScheduleShiftSlot(1, false, false, 1, now, now.plus({ hours: 1 }), new Set()),
new ClientScheduleShiftSlot(2, false, false, multiSlot ? 1 : 2, now, now.plus({ hours: 2 }), new Set()),
]);
const schedule = new ClientSchedule(
111,
now,
false,
new ClientMap(ClientScheduleLocation, idMap([left, right]), new Map()),
new ClientMap(ClientScheduleEvent, idMap(events), new Map()),
eventSlots,
new ClientMap(ClientScheduleRole, idMap([red, blue]), new Map()),
new ClientMap(ClientScheduleShift, idMap(shifts), new Map()),
shiftSlots,
);
for (const event of events.values()) {
event.schedule = schedule;
}
for (const eventSlot of eventSlots.values()) {
eventSlot.schedule = schedule;
}
for (const shift of shifts.values()) {
shift.schedule = schedule;
}
for (const shiftSlot of shiftSlots.values()) {
shiftSlot.schedule = schedule;
}
return schedule;
}
function fixtureApiSchedule(): 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 entityTests: [string, (schedule: ClientSchedule) => ClientEntity<ApiEntity>][] = [
[
"location",
() => ClientScheduleLocation.create(3, "New location", "", { zone, locale })
],
[
"event",
(schedule) => ClientScheduleEvent.create(schedule, 3, "New location", false, "", false, "", 0, new Set(), { zone, locale })
],
[
"role",
() => ClientScheduleRole.create(3, "New location", "", { zone, locale })
],
[
"shift",
(schedule) => ClientScheduleShift.create(schedule, 3, 1, "New location", "", new Set(), { zone, locale })
],
] 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);
// Create
(schedule as any)[`${name}s`].add(entity);
// Check
expect(schedule.isModified()).toBe(true);
expect((schedule as any)[`${name}s`].get(entity.id).isModified()).toBe(true);
expect((schedule as any)[`${name}s`].get(entity.id).isNew()).toBe(true);
expect((schedule as any)[`${name}s`].get(entity.id)).toBe(entity);
});
test("edit", () => {
const schedule = fixtureClientSchedule();
const entity = (schedule as any)[`${name}s`].get(1);
expect(schedule.isModified()).toBe(false);
expect(entity.isModified()).toBe(false);
const originalName = entity.name;
// Edit
entity.name = `Modified ${name}`;
// Check
expect(schedule.isModified()).toBe(true);
expect(entity.isModified()).toBe(true);
expect(entity.serverName).toBe(originalName);
});
if (name === "location") {
test.skip("delete location in use throws", () => {
const schedule = fixtureClientSchedule();
expect(
() => { schedule.locations.get(1)!.deleted = true; }
).toThrow(new Error('Cannot delete location, event "Up" depends on it'));
});
} else if (name === "role") {
test.skip("delete role in use throws", () => {
const schedule = fixtureClientSchedule();
expect(
() => { schedule.roles.get(1)!.deleted = true; }
).toThrow(new Error('Cannot delete role, shift "White" depends on it'));
});
}
test("delete", () => {
const schedule = fixtureClientSchedule();
const entity = (schedule as any)[`${name}s`].get(1);
expect(schedule.isModified()).toBe(false);
expect(entity.isModified()).toBe(false);
// Delete
if (name === "location") {
schedule.events.get(1)!.deleted = true;
} else if (name === "role") {
schedule.shifts.get(1)!.deleted = true;
}
entity.deleted = true;
// Check
expect(schedule.isModified()).toBe(true);
expect(entity.isModified()).toBe(true);
expect(entity.serverDeleted).toBe(false);
expect(entity.deleted).toBe(true);
});
});
}
describe("event slot", () => {
test("edit", () => {
const schedule = fixtureClientSchedule();
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
// Modify
schedule.eventSlots.get(1)!.start = later;
// Check
expect(schedule.isModified()).toBe(true);
expect(schedule.events.get(1)!.isModified()).toBe(true);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(true);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
test("add and apply", () => {
const schedule = fixtureClientSchedule();
// Add
const slot = ClientScheduleEventSlot.create(
schedule,
3,
1,
now,
now.plus({ hours: 3 }),
new Set(),
new Set(),
0,
);
schedule.eventSlots.set(slot.id, slot);
schedule.events.get(1)!.slotIds.add(slot.id);
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
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: 3,
start: nowIso,
end: toIso(now.plus({ hours: 3 })),
locationIds: [],
},
],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(3)!.isNewEntity).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(2);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
test("edit and apply", () => {
const schedule = fixtureClientSchedule();
// Modify
schedule.eventSlots.get(1)!.locationIds.add(2);
schedule.eventSlots.get(1)!.locationIds.delete(1);
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
events: [{
id: 1,
updatedAt: laterIso,
name: "Up",
description: "What's Up?",
slots: [{
id: 1,
start: nowIso,
end: toIso(now.plus({ hours: 1 })),
locationIds: [2],
}],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.locationIds).toEqual(new Set([2]));
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
test("delete and apply", () => {
const schedule = fixtureClientSchedule(true);
// delete
schedule.eventSlots.get(1)!.deleted = true;
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
events: [{
id: 1,
updatedAt: laterIso,
name: "Up",
description: "What's Up?",
slots: [{
id: 2,
start: nowIso,
end: toIso(now.plus({ hours: 2 })),
locationIds: [2],
}],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.has(1)).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(0);
});
test("move to another event", () => {
const schedule = fixtureClientSchedule();
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
// Modify
schedule.eventSlots.get(1)!.setEventId(2);
// Check
expect(schedule.isModified()).toBe(true);
expect(schedule.events.get(1)!.isModified()).toBe(true);
expect(schedule.events.get(2)!.isModified()).toBe(true);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(true);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(0);
expect(schedule.events.get(2)!.slots.size).toBe(2);
// Move back
schedule.eventSlots.get(1)!.setEventId(1);
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
test("discard", () => {
const schedule = fixtureClientSchedule();
schedule.eventSlots.get(1)!.start = later;
schedule.eventSlots.get(1)!.discard();
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
test("discard from another event", () => {
const schedule = fixtureClientSchedule();
schedule.eventSlots.get(1)!.setEventId(2);
schedule.eventSlots.get(1)!.discard();
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.events.get(1)!.isModified()).toBe(false);
expect(schedule.events.get(2)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(1)!.isModified()).toBe(false);
expect(schedule.eventSlots.get(2)!.isModified()).toBe(false);
expect(schedule.events.get(1)!.slots.size).toBe(1);
expect(schedule.events.get(2)!.slots.size).toBe(1);
});
});
describe("shift slot", () => {
test("edit", () => {
const schedule = fixtureClientSchedule();
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
// Modify
schedule.shiftSlots.get(1)!.start = later;
// Check
expect(schedule.isModified()).toBe(true);
expect(schedule.shifts.get(1)!.isModified()).toBe(true);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(true);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
});
test("add and apply", () => {
const schedule = fixtureClientSchedule();
// Add
const slot = ClientScheduleShiftSlot.create(
schedule,
3,
1,
now,
now.plus({ hours: 3 }),
new Set(),
);
schedule.shiftSlots.set(slot.id, slot);
schedule.shifts.get(1)!.slotIds.add(slot.id);
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
shifts: [{
id: 1,
updatedAt: nowIso,
name: "White",
roleId: 1,
slots: [
{
id: 1,
start: nowIso,
end: toIso(now.plus({ hours: 1 })),
},
{
id: 3,
start: nowIso,
end: toIso(now.plus({ hours: 3 })),
},
],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(3)!.isNewEntity).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(2);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
});
test("edit and apply", () => {
const schedule = fixtureClientSchedule();
// Modify
schedule.shiftSlots.get(1)!.assigned.add(2);
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
shifts: [{
id: 1,
updatedAt: nowIso,
name: "White",
roleId: 1,
slots: [{
id: 1,
start: nowIso,
end: toIso(now.plus({ hours: 1 })),
assigned: [2],
}],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.assigned).toEqual(new Set([2]));
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
});
test("delete and apply", () => {
const schedule = fixtureClientSchedule(true);
// delete
schedule.shiftSlots.get(1)!.deleted = true;
// Sanity check
expect(schedule.isModified()).toBe(true);
// Apply change
schedule.apiUpdate({
id: 111,
updatedAt: laterIso,
shifts: [{
id: 1,
updatedAt: nowIso,
name: "White",
roleId: 1,
slots: [{
id: 2,
start: nowIso,
end: toIso(now.plus({ hours: 2 })),
}],
}],
}, { zone, locale });
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.has(1)).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(0);
});
test("move to another shift", () => {
const schedule = fixtureClientSchedule();
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
// Modify
schedule.shiftSlots.get(1)!.setShiftId(2);
// Check
expect(schedule.isModified()).toBe(true);
expect(schedule.shifts.get(1)!.isModified()).toBe(true);
expect(schedule.shifts.get(2)!.isModified()).toBe(true);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(true);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(0);
expect(schedule.shifts.get(2)!.slots.size).toBe(2);
// Move back
schedule.shiftSlots.get(1)!.setShiftId(1);
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
});
test("discard", () => {
const schedule = fixtureClientSchedule();
schedule.shiftSlots.get(1)!.start = later;
schedule.shiftSlots.get(1)!.discard();
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
});
test("discard from another shift", () => {
const schedule = fixtureClientSchedule();
schedule.shiftSlots.get(1)!.setShiftId(2);
schedule.shiftSlots.get(1)!.discard();
// Check
expect(schedule.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.isModified()).toBe(false);
expect(schedule.shifts.get(2)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(1)!.isModified()).toBe(false);
expect(schedule.shiftSlots.get(2)!.isModified()).toBe(false);
expect(schedule.shifts.get(1)!.slots.size).toBe(1);
expect(schedule.shifts.get(2)!.slots.size).toBe(1);
});
});
});