Add recurrence testing, use dayjs for calendar

This commit is contained in:
BeatLink 2026-02-27 17:39:12 -05:00
parent 8554dc249c
commit e029379194
2 changed files with 93 additions and 12 deletions

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { buildNote, buildNotes } from "../../../test/easy-froca.js";
import { buildEvent, buildEvents } from "./event_builder.js";
import { LOCALE_MAPPINGS } from "./index.js";
@ -148,7 +148,7 @@ describe("Promoted attributes", () => {
expect(event).toHaveLength(1);
expect(event[0]?.promotedAttributes).toMatchObject([
[ "assignee", "Target note" ]
])
]);
});
it("supports start time and end time", async () => {
@ -177,6 +177,86 @@ describe("Promoted attributes", () => {
});
describe("Recurrence", () => {
it("supports valid recurrence without end date", async () => {
const noteIds = buildNotes([
{
title: "Recurring Event",
"#startDate": "2025-05-05",
"#recurrence": "FREQ=DAILY;COUNT=5"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
title: "Recurring Event",
start: "2025-05-05",
});
expect(events[0].rrule).toContain("DTSTART:20250505");
expect(events[0].rrule).toContain("FREQ=DAILY;COUNT=5");
expect(events[0].end).toBeUndefined();
});
it("supports recurrence with start and end time (duration calculated)", async () => {
const noteIds = buildNotes([
{
title: "Timed Recurring Event",
"#startDate": "2025-05-05",
"#startTime": "13:00",
"#endTime": "15:30",
"#recurrence": "FREQ=WEEKLY;COUNT=3"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
title: "Timed Recurring Event",
start: "2025-05-05T13:00:00",
duration: "02:30"
});
expect(events[0].rrule).toContain("DTSTART:20250505T130000");
expect(events[0].end).toBeUndefined();
});
it("removes end date when recurrence is valid", async () => {
const noteIds = buildNotes([
{
title: "Recurring With End",
"#startDate": "2025-05-05",
"#endDate": "2025-05-07",
"#recurrence": "FREQ=DAILY;COUNT=2"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0].rrule).toBeDefined();
expect(events[0].end).toBeUndefined();
});
it("writes to console on invalid recurrence rule", async () => {
const noteIds = buildNotes([
{
title: "Invalid Recurrence",
"#startDate": "2025-05-05",
"#recurrence": "RRULE:FREQ=INVALID"
}
]);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await buildEvents(noteIds);
const calledWithInvalid = consoleSpy.mock.calls.some(call =>
call[0].includes("has an invalid #recurrence string")
);
expect(calledWithInvalid).toBe(true);
consoleSpy.mockRestore();
});
});
describe("Building locales", () => {
it("every language has a locale defined", async () => {
for (const { id, contentOnly } of LOCALES) {

View File

@ -1,12 +1,14 @@
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
import { dayjs } from "@triliumnext/commons";
import clsx from "clsx";
import { start } from "repl";
import * as rruleLib from 'rrule';
import FNote from "../../../entities/fnote";
import froca from "../../../services/froca";
import server from "../../../services/server";
import toastService from "../../../services/toast";
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
import { getCustomisableLabel, getMonthsInDateRange } from "./utils";
interface Event {
startDate: string,
@ -127,9 +129,10 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
startDate = (startTime ? `${startDate}T${startTime}:00` : startDate);
if (!startTime) {
const endDateOffset = offsetDate(endDate ?? startDate, 1);
if (endDateOffset) {
endDate = formatDateToLocalISO(endDateOffset);
if (endDate) {
endDate = dayjs(endDate).add(1, "day").format("YYYY-MM-DD");
} else if (startDate) {
endDate = dayjs(startDate).add(1, "day").format("YYYY-MM-DD");
}
}
@ -150,7 +153,7 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
if (recurrence) {
// Generate rrule string
const rruleString = `DTSTART:${startDate.replace(/[-:]/g, "")}\n${recurrence}`;
const rruleString = `DTSTART:${dayjs(startDate).format("YYYYMMDD[T]HHmmss")}\n${recurrence}`;
// Validate rrule string
let rruleValid = true;
@ -164,13 +167,11 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
delete eventData.end;
eventData.rrule = rruleString;
if (endDate){
const diffMs = new Date(endDate).getTime() - new Date(startDate).getTime();
const hours = Math.floor(diffMs / 3600000);
const minutes = Math.floor((diffMs / 60000) % 60);
eventData.duration = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
const duration = dayjs.duration(dayjs(endDate).diff(dayjs(startDate)));
eventData.duration = duration.format("HH:mm");
}
} else {
throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string. Excluding...`);
throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string ${recurrence}. Excluding...`);
}
}
events.push(eventData);