owltide/utils/client-schedule.nuxt.test.ts
Hornwitser fad1bb2482
All checks were successful
/ build (push) Successful in 1m28s
/ deploy (push) Successful in 16s
Ignore updatedAt timestamp when comparing entities
To determine if the entity received from the server is the same as the
entity the client has the equals method is use.  To avoid problems with
clients that have incorrect clocks the timestamp is overriden with the
server's time when the entities are saved on the server.

This means that the entities received back from the server when the
client saves will have different timestamps than what the client set.
Ignore the updatedAt timestamp when comparing entities for equality so
that the update logic correctly replaces entities that only differ by
the update timestamp.
2025-06-13 21:06:48 +02:00

292 lines
8.4 KiB
TypeScript

import { ClientEntity, 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 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,
[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 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, [])
],
[
"role",
() => new ClientScheduleRole(3, now, false, "New location", "")
],
[
"shift",
(schedule) => new ClientScheduleShift(3, now, false, schedule.roles.get(1)!, "New location", "", [])
],
] 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[0].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);
});
});
}
});