owltide/utils/client-schedule.nuxt.test.ts
Hornwitser e100555304
All checks were successful
/ build (push) Successful in 2m16s
/ deploy (push) Successful in 16s
Implement ClientSchedule state tracking class
Write the logic of keeping track of location modifications and applying
updates from the server into the ClientSchedule class.  This should
serve as the foundation for replacing the prototype in-component update
logic which have turned into an unmaintainable spagetti.
2025-06-12 21:48:50 +02:00

284 lines
7.5 KiB
TypeScript

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<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 fixture: 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 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);
});
});