Files
margin-calendar/src/components/EventDetailsModel.test.ts
pj 661100dfdc Margin Calendar: a Google Calendar client for desktop and phone
Tauri 2, React 19 and zustand on the front, Rust behind. Rust owns auth,
all HTTP to Google, the SQLite store, the sync loop, recurrence expansion
and timezone maths. TypeScript owns rendering and never talks to Google,
which keeps the content security policy locked to ipc:.

Week, day and agenda views, and no month view: it would be a second layout
engine, and the fit and fold logic that makes a day fit the window without
scrolling is the whole point of the app.

Runs on macOS, Linux, Android and iOS. Desktop catches Google's OAuth
redirect on a loopback port. A phone cannot, and Google rejects loopback
for mobile client types anyway, so it redirects to a custom URI scheme and
needs its own public OAuth clients, which docs/mobile.md covers. Refresh
tokens are sealed with XChaCha20-Poly1305 in the app data directory on
every platform, with no OS credential store in the picture.

On a phone the chrome becomes a top bar and a bottom tab bar, overlays
become sheets, hover affordances become taps, and dragging out an event
waits for a long press. Navigation moves one day at a time everywhere,
a swipe included.
2026-08-12 17:09:21 +05:30

170 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
badgesOf,
conferenceLabel,
isStrandedAnchor,
orderGuests,
place,
type Bounds,
} from "./EventDetailsModel";
import type { Attendee, Instance } from "../ipc";
const bounds: Bounds = { top: 54, left: 8, right: 1192, bottom: 792 };
const card = { width: 336, height: 300 };
describe("place", () => {
it("prefers the right of the block, aligned to its top", () => {
const at = place({ top: 200, left: 300, width: 120, height: 40 }, card, bounds);
expect(at).toEqual({ top: 200, left: 430, side: "right" });
});
it("flips to the left when the right would not fit", () => {
const at = place({ top: 200, left: 1000, width: 150, height: 40 }, card, bounds);
expect(at).toEqual({ top: 200, left: 654, side: "left" });
});
it("goes below when neither side fits, centred across the block", () => {
const narrow: Bounds = { top: 54, left: 8, right: 420, bottom: 792 };
const at = place({ top: 100, left: 120, width: 160, height: 40 }, card, narrow);
expect(at.side).toBe("below");
expect(at.top).toBe(150);
expect(at.left).toBe(32);
});
it("goes above when there is no room below", () => {
const shallow: Bounds = { top: 54, left: 8, right: 420, bottom: 600 };
const at = place({ top: 500, left: 120, width: 160, height: 40 }, card, shallow);
expect(at).toEqual({ top: 190, left: 32, side: "above" });
});
it("never lets the card hang off the top or the bottom", () => {
const high = place({ top: 30, left: 300, width: 120, height: 60 }, { width: 336, height: 700 }, bounds);
expect(high.top).toBe(54);
const low = place({ top: 700, left: 300, width: 120, height: 20 }, card, bounds);
expect(low.top).toBe(492);
expect(low.top + card.height).toBeLessThanOrEqual(bounds.bottom);
});
it("takes the middle of the window when the anchor has no size", () => {
const at = place({ top: 400, left: 600, width: 0, height: 0 }, card, bounds);
expect(at.side).toBe("centre");
expect(at.left).toBe(432);
expect(at.top).toBe(273);
});
it("takes the middle of the window when there is no anchor at all", () => {
expect(place(null, card, bounds).side).toBe("centre");
});
it("stays inside bounds smaller than the card itself", () => {
const tiny: Bounds = { top: 54, left: 8, right: 200, bottom: 200 };
const at = place({ top: 100, left: 20, width: 40, height: 10 }, card, tiny);
expect(at.top).toBe(54);
expect(at.left).toBe(8);
});
});
describe("isStrandedAnchor", () => {
it("catches a rectangle scrolled off the top", () => {
expect(isStrandedAnchor({ top: -80, left: 300, width: 100, height: 40 }, bounds)).toBe(true);
});
it("catches a rectangle past the right edge", () => {
expect(isStrandedAnchor({ top: 200, left: 1400, width: 100, height: 40 }, bounds)).toBe(true);
});
it("accepts a rectangle only partly on screen", () => {
expect(isStrandedAnchor({ top: 20, left: 300, width: 100, height: 80 }, bounds)).toBe(false);
});
});
const instance = (over: Partial<Instance> = {}): Instance => ({
colorId: null,
eventId: "ev-1",
calendarId: "cal-1",
accountId: "acct-1",
originalStart: null,
start: "2026-08-11T09:00:00+05:30",
end: "2026-08-11T09:30:00+05:30",
startMs: 0,
endMs: 0,
allDay: false,
summary: "Stand-up",
description: null,
location: null,
status: "confirmed",
recurring: false,
colorHex: "#6f6194",
etag: null,
organizer: null,
attendees: [],
conference: null,
readOnly: false,
pending: false,
...over,
});
describe("badgesOf", () => {
it("says nothing about an ordinary event", () => {
expect(badgesOf(instance())).toEqual([]);
});
it("names everything unusual about the occurrence", () => {
expect(badgesOf(instance({ status: "tentative", recurring: true, pending: true, readOnly: true }))).toEqual([
"tentative",
"repeats",
"not synced yet",
"read only",
]);
});
});
describe("conferenceLabel", () => {
it("prefers the label Google sent", () => {
const c = { kind: "hangoutsMeet", uri: "https://meet.google.com/a", label: "meet.google.com/a" };
expect(conferenceLabel(instance({ conference: c }))).toBe("meet.google.com/a");
});
it("names Meet when there is no label", () => {
const c = { kind: "hangoutsMeet", uri: "https://meet.google.com/a", label: null };
expect(conferenceLabel(instance({ conference: c }))).toBe("Google Meet");
});
it("is empty when there is no conference", () => {
expect(conferenceLabel(instance())).toBe("");
});
});
const guest = (over: Partial<Attendee>): Attendee => ({
email: "[email protected]",
displayName: null,
responseStatus: "needsAction",
organizer: false,
self: false,
optional: false,
...over,
});
describe("orderGuests", () => {
it("puts the organiser first and the declines last", () => {
const list = [
guest({ email: "[email protected]", responseStatus: "declined" }),
guest({ email: "[email protected]", responseStatus: "accepted" }),
guest({ email: "[email protected]", responseStatus: "needsAction", organizer: true }),
guest({ email: "[email protected]", responseStatus: "tentative" }),
];
expect(orderGuests(list).map((a) => a.email)).toEqual([
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]);
});
it("does not mutate what it was handed", () => {
const list = [guest({ email: "[email protected]", responseStatus: "declined" }), guest({ email: "[email protected]" })];
orderGuests(list);
expect(list[0].email).toBe("[email protected]");
});
});