|
|
|
|
@@ -48,12 +48,13 @@ class MockCredentialProvider implements CalendarCredentialProvider {
|
|
|
|
|
|
|
|
|
|
class MockDAVClient implements CalendarDAVClient {
|
|
|
|
|
credentials: Record<string, unknown> = {}
|
|
|
|
|
fetchCalendarsCallCount = 0
|
|
|
|
|
private calendars: CalendarDAVCalendar[]
|
|
|
|
|
private objectsByCalendarUrl: Map<string, CalendarDAVObject[]>
|
|
|
|
|
private objectsByCalendarUrl: Record<string, CalendarDAVObject[]>
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
calendars: CalendarDAVCalendar[],
|
|
|
|
|
objectsByCalendarUrl: Map<string, CalendarDAVObject[]>,
|
|
|
|
|
objectsByCalendarUrl: Record<string, CalendarDAVObject[]>,
|
|
|
|
|
) {
|
|
|
|
|
this.calendars = calendars
|
|
|
|
|
this.objectsByCalendarUrl = objectsByCalendarUrl
|
|
|
|
|
@@ -62,6 +63,7 @@ class MockDAVClient implements CalendarDAVClient {
|
|
|
|
|
async login(): Promise<void> {}
|
|
|
|
|
|
|
|
|
|
async fetchCalendars(): Promise<CalendarDAVCalendar[]> {
|
|
|
|
|
this.fetchCalendarsCallCount++
|
|
|
|
|
return this.calendars
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -69,7 +71,7 @@ class MockDAVClient implements CalendarDAVClient {
|
|
|
|
|
calendar: CalendarDAVCalendar
|
|
|
|
|
timeRange: { start: string; end: string }
|
|
|
|
|
}): Promise<CalendarDAVObject[]> {
|
|
|
|
|
return this.objectsByCalendarUrl.get(params.calendar.url) ?? []
|
|
|
|
|
return this.objectsByCalendarUrl[params.calendar.url] ?? []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -86,7 +88,7 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns empty array when no calendars exist", async () => {
|
|
|
|
|
const client = new MockDAVClient([], new Map())
|
|
|
|
|
const client = new MockDAVClient([], {})
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
})
|
|
|
|
|
@@ -95,9 +97,9 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns feed items from a single calendar", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -116,13 +118,12 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns feed items from multiple calendars", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }]],
|
|
|
|
|
[
|
|
|
|
|
"/cal/personal",
|
|
|
|
|
[{ url: "/cal/personal/event2.ics", data: loadFixture("all-day-event.ics") }],
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
"/cal/personal": [
|
|
|
|
|
{ url: "/cal/personal/event2.ics", data: loadFixture("all-day-event.ics") },
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient(
|
|
|
|
|
[
|
|
|
|
|
{ url: "/cal/work", displayName: "Work" },
|
|
|
|
|
@@ -150,16 +151,13 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("skips objects with non-string data", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
[
|
|
|
|
|
"/cal/work",
|
|
|
|
|
[
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [
|
|
|
|
|
{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") },
|
|
|
|
|
{ url: "/cal/work/bad.ics", data: 12345 },
|
|
|
|
|
{ url: "/cal/work/empty.ics" },
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -171,9 +169,9 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("uses context time as feed item timestamp", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -185,15 +183,12 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("assigns priority based on event proximity", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
[
|
|
|
|
|
"/cal/work",
|
|
|
|
|
[
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [
|
|
|
|
|
{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") },
|
|
|
|
|
{ url: "/cal/work/allday.ics", data: loadFixture("all-day-event.ics") },
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -210,9 +205,9 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("handles calendar with non-string displayName", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/weird", [{ url: "/cal/weird/event1.ics", data: loadFixture("minimal-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/weird": [{ url: "/cal/weird/event1.ics", data: loadFixture("minimal-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient(
|
|
|
|
|
[{ url: "/cal/weird", displayName: { _cdata: "Weird Calendar" } }],
|
|
|
|
|
objects,
|
|
|
|
|
@@ -226,9 +221,9 @@ describe("CalendarSource", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("handles recurring events with exceptions", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/recurring.ics", data: loadFixture("recurring-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/recurring.ics", data: loadFixture("recurring-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -248,6 +243,40 @@ describe("CalendarSource", () => {
|
|
|
|
|
expect(exception!.data.recurrenceId).not.toBeNull()
|
|
|
|
|
expect(exception!.id).toContain("-")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("caches events within the same refresh cycle", async () => {
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const context = createContext(new Date("2026-01-15T12:00:00Z"))
|
|
|
|
|
|
|
|
|
|
await source.fetchContext(context)
|
|
|
|
|
await source.fetchItems(context)
|
|
|
|
|
|
|
|
|
|
// Same context.time reference — fetchEvents should only hit the client once
|
|
|
|
|
expect(client.fetchCalendarsCallCount).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("refetches events for a different context time", async () => {
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await source.fetchItems(createContext(new Date("2026-01-15T12:00:00Z")))
|
|
|
|
|
await source.fetchItems(createContext(new Date("2026-01-15T13:00:00Z")))
|
|
|
|
|
|
|
|
|
|
// Different context.time references — should fetch twice
|
|
|
|
|
expect(client.fetchCalendarsCallCount).toBe(2)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("CalendarSource.fetchContext", () => {
|
|
|
|
|
@@ -264,9 +293,9 @@ describe("CalendarSource.fetchContext", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("identifies in-progress events", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -281,9 +310,9 @@ describe("CalendarSource.fetchContext", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("identifies next upcoming event", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -299,9 +328,9 @@ describe("CalendarSource.fetchContext", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("excludes all-day events from inProgress and nextEvent", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
["/cal/work", [{ url: "/cal/work/allday.ics", data: loadFixture("all-day-event.ics") }]],
|
|
|
|
|
])
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [{ url: "/cal/work/allday.ics", data: loadFixture("all-day-event.ics") }],
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
@@ -317,15 +346,12 @@ describe("CalendarSource.fetchContext", () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("counts all events including all-day in todayEventCount", async () => {
|
|
|
|
|
const objects = new Map<string, CalendarDAVObject[]>([
|
|
|
|
|
[
|
|
|
|
|
"/cal/work",
|
|
|
|
|
[
|
|
|
|
|
const objects: Record<string, CalendarDAVObject[]> = {
|
|
|
|
|
"/cal/work": [
|
|
|
|
|
{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") },
|
|
|
|
|
{ url: "/cal/work/allday.ics", data: loadFixture("all-day-event.ics") },
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
|
|
|
|
const source = new CalendarSource(new MockCredentialProvider(), "user-1", {
|
|
|
|
|
davClient: client,
|
|
|
|
|
|