joplin-plugin-gtd-calendar/src/tests/Gtd/groupEvents.test.ts
Victor Wiebe cdb5ad022c v0.2.0: mark recurring to-dos with a recurrence symbol
- Detect recurrence via the 'recurring' tag maintained by the Repeating
  To-Dos plugin (v2 stores recurrence in userData, not the note body, so
  tag-presence is the correct signal — not body scanning)
- DataAdapter gains getNoteTagTitles; lookup runs for to-dos only (notes
  can't recur), keeping extra API calls minimal
- GtdEvent.isRecurring threaded through collector and payload
- Webview shows ↻ to the right of any icon/glyph across all views:
  strip tiles, hover-card rows, month-grid chips, unscheduled chips
- RECURRING_TAG constant leaves room for a future configurable tag name
- README documents the pairing; SPEC roadmap marks Option A shipped
- Version bumped to 0.2.0; 2 new tests (49 total)
2026-06-13 09:49:20 -04:00

195 lines
6.6 KiB
TypeScript

import groupEvents from "../../Gtd/groupEvents";
import { GtdEvent } from "../../Gtd/types";
function makeEvent(overrides: Partial<GtdEvent>): GtdEvent {
return {
id: "id",
title: "Event",
date: null,
isTodo: false,
completed: false,
bgColour: null,
fgColour: null,
icon: null,
text: null,
updatedTime: 0,
isRecurring: false,
...overrides,
};
}
const TODAY = new Date(2026, 5, 12); // Jun 12 2026
describe("groupEvents — day view", () => {
test("one group per calendar day across the span, empty groups included", () => {
const events = [
makeEvent({ id: "a", date: "2026-06-10" }),
makeEvent({ id: "b", date: "2026-06-14" }),
];
const result = groupEvents(events, "day", TODAY);
// Jun 10 .. Jun 14 (today Jun 12 is inside the span)
expect(result.groups).toHaveLength(5);
expect(result.groups[0].events.map((e) => e.id)).toEqual(["a"]);
expect(result.groups[1].events).toHaveLength(0);
expect(result.groups[4].events.map((e) => e.id)).toEqual(["b"]);
});
test("span always includes today", () => {
const events = [makeEvent({ id: "past", date: "2026-06-08" })];
const result = groupEvents(events, "day", TODAY);
// Jun 8 .. Jun 12
expect(result.groups).toHaveLength(5);
expect(result.groups[4].isCurrent).toBe(true);
expect(result.groups[4].dateISO).toBe("2026-06-12");
});
test("first-of-month label is the month name and primary", () => {
const events = [
makeEvent({ id: "a", date: "2026-06-30" }),
makeEvent({ id: "b", date: "2026-07-02" }),
];
const result = groupEvents(events, "day", new Date(2026, 6, 1));
const firstOfJuly = result.groups.find(
(g) => g.dateISO === "2026-07-01"
)!;
expect(firstOfJuly.labelPrimary).toBe(true);
expect(firstOfJuly.label.toLowerCase()).toContain("jul");
expect(firstOfJuly.monthIndex).toBe(6);
});
test("multiple events on one day share a group, order preserved", () => {
const events = [
makeEvent({ id: "x", date: "2026-06-12" }),
makeEvent({ id: "y", date: "2026-06-12" }),
];
const result = groupEvents(events, "day", TODAY);
expect(result.groups).toHaveLength(1);
expect(result.groups[0].events.map((e) => e.id)).toEqual(["x", "y"]);
});
});
describe("groupEvents — week and month views", () => {
test("week view groups by calendar week with current-week flag", () => {
const events = [
makeEvent({ id: "a", date: "2026-06-01" }),
makeEvent({ id: "b", date: "2026-06-19" }),
];
const result = groupEvents(events, "week", TODAY);
// Weeks of May 31, Jun 7, Jun 14 → 3 groups
expect(result.groups).toHaveLength(3);
expect(result.groups[1].isCurrent).toBe(true); // week containing Jun 12
expect(result.groups[0].events.map((e) => e.id)).toEqual(["a"]);
expect(result.groups[2].events.map((e) => e.id)).toEqual(["b"]);
});
test("month view groups by calendar month", () => {
const events = [
makeEvent({ id: "a", date: "2026-04-20" }),
makeEvent({ id: "b", date: "2026-08-03" }),
];
const result = groupEvents(events, "month", TODAY);
// Apr, May, Jun, Jul, Aug
expect(result.groups).toHaveLength(5);
expect(result.groups[2].isCurrent).toBe(true); // June
expect(result.groups[0].events.map((e) => e.id)).toEqual(["a"]);
expect(result.groups[4].events.map((e) => e.id)).toEqual(["b"]);
});
test("january month label is the year and primary", () => {
const events = [
makeEvent({ id: "a", date: "2025-12-15" }),
makeEvent({ id: "b", date: "2026-01-10" }),
];
const result = groupEvents(events, "month", new Date(2026, 0, 5));
const january = result.groups.find((g) => g.monthIndex === 0)!;
expect(january.label).toBe("2026");
expect(january.labelPrimary).toBe(true);
});
});
describe("groupEvents — unscheduled", () => {
test("dateless events pass through in order; no groups without scheduled events", () => {
const events = [
makeEvent({ id: "u1" }),
makeEvent({ id: "u2" }),
];
const result = groupEvents(events, "day", TODAY);
expect(result.groups).toHaveLength(0);
expect(result.unscheduled.map((e) => e.id)).toEqual(["u1", "u2"]);
});
test("mixed: scheduled grouped, dateless set aside", () => {
const events = [
makeEvent({ id: "s", date: "2026-06-12" }),
makeEvent({ id: "u" }),
];
const result = groupEvents(events, "day", TODAY);
expect(result.groups[0].events.map((e) => e.id)).toEqual(["s"]);
expect(result.unscheduled.map((e) => e.id)).toEqual(["u"]);
});
});
describe("groupEvents — month-grid view", () => {
test("covers the whole current month padded to full weeks", () => {
const result = groupEvents([], "month-grid", TODAY);
const grid = result.grid!;
expect(grid.cells.length % 7).toBe(0);
expect(grid.weekdays).toHaveLength(7);
expect(grid.monthLabel).toContain("2026");
const inMonth = grid.cells.filter((c) => c.inMonth);
expect(inMonth).toHaveLength(30); // June 2026
expect(inMonth[0].dayNumber).toBe(1);
expect(inMonth[29].dayNumber).toBe(30);
});
test("flags today and buckets events into the right cells", () => {
const events = [
makeEvent({ id: "a", date: "2026-06-12" }),
makeEvent({ id: "b", date: "2026-06-12" }),
makeEvent({ id: "c", date: "2026-06-25" }),
];
const grid = groupEvents(events, "month-grid", TODAY).grid!;
const todayCell = grid.cells.find((c) => c.isToday)!;
expect(todayCell.dateISO).toBe("2026-06-12");
expect(todayCell.events.map((e) => e.id)).toEqual(["a", "b"]);
const cell25 = grid.cells.find((c) => c.dateISO === "2026-06-25")!;
expect(cell25.events.map((e) => e.id)).toEqual(["c"]);
});
test("events outside the displayed window are counted, not shown", () => {
const events = [
makeEvent({ id: "in", date: "2026-06-05" }),
makeEvent({ id: "out", date: "2026-09-01" }),
];
const grid = groupEvents(events, "month-grid", TODAY).grid!;
expect(grid.outsideCount).toBe(1);
const allShown = grid.cells.flatMap((c) => c.events.map((e) => e.id));
expect(allShown).toEqual(["in"]);
});
test("adjacent-month padding days can still hold events", () => {
// June 2026 starts on a Monday; the grid window starts Sunday May 31.
const events = [makeEvent({ id: "pad", date: "2026-05-31" })];
const grid = groupEvents(events, "month-grid", TODAY).grid!;
const padCell = grid.cells.find((c) => c.dateISO === "2026-05-31")!;
expect(padCell.inMonth).toBe(false);
expect(padCell.events.map((e) => e.id)).toEqual(["pad"]);
expect(grid.outsideCount).toBe(0);
});
test("unscheduled events still pass through in month-grid view", () => {
const events = [makeEvent({ id: "u" })];
const result = groupEvents(events, "month-grid", TODAY);
expect(result.unscheduled.map((e) => e.id)).toEqual(["u"]);
expect(result.groups).toHaveLength(0);
});
});