mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
A calendar holding only tasks (VTODO, e.g. from tasks.org) appeared in the event calendar UI as an empty calendar. Stalwart's Calendar/get exposes no supported-component set, so tasks-only-ness is derived from the calendar's objects (mirroring the existing event/task split): a calendar is tasks-only iff it holds >=1 object and every object is a task. - getTasksOnlyCalendarIds scans an account's objects (CalendarEvent/query + minimal /get), early-exits once every calendar has an event, only marks after a full scan, and fails open on the cap or any error. - getAllCalendars/getCalendars annotate calendars with isTasksOnly. - Such calendars stay in the store for the tasks view (task picker, task-list grouping) but are filtered from the event sidebar and default selection. Verified against Stalwart v0.16.16: the tasks-only calendar is flagged, the default (with an event) is not. Closes #761.
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { isTaskLikeObject, findTasksOnlyCalendarIds, type ScannedCalendarObject } from '@/lib/calendar-component-detection';
|
|
|
|
describe('isTaskLikeObject', () => {
|
|
it('treats an explicit @type Task as a task', () => {
|
|
expect(isTaskLikeObject({ '@type': 'Task' })).toBe(true);
|
|
expect(isTaskLikeObject({ '@type': 'task' })).toBe(true);
|
|
});
|
|
|
|
it('treats an @type Event as not a task even with a stray field', () => {
|
|
expect(isTaskLikeObject({ '@type': 'Event' })).toBe(false);
|
|
expect(isTaskLikeObject({ '@type': 'Event', due: '2026-01-01T00:00:00' })).toBe(false);
|
|
});
|
|
|
|
it('detects a CalDAV task lacking @type by its task-only fields', () => {
|
|
expect(isTaskLikeObject({ due: '2026-01-01T00:00:00' })).toBe(true);
|
|
expect(isTaskLikeObject({ progress: 'needs-action' })).toBe(true);
|
|
expect(isTaskLikeObject({ percentComplete: 0 })).toBe(true);
|
|
});
|
|
|
|
it('treats a plain object with no task markers as an event', () => {
|
|
expect(isTaskLikeObject({})).toBe(false);
|
|
expect(isTaskLikeObject({ '@type': 'Event', title: 'Standup' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('findTasksOnlyCalendarIds', () => {
|
|
it('flags a calendar whose objects are all tasks', () => {
|
|
const objects: ScannedCalendarObject[] = [
|
|
{ '@type': 'Task', calendarIds: { 'cal-tasks': true } },
|
|
{ due: '2026-01-01T00:00:00', calendarIds: { 'cal-tasks': true } },
|
|
];
|
|
expect([...findTasksOnlyCalendarIds(objects, ['cal-tasks'])]).toEqual(['cal-tasks']);
|
|
});
|
|
|
|
it('does not flag a calendar that has at least one event', () => {
|
|
const objects: ScannedCalendarObject[] = [
|
|
{ '@type': 'Task', calendarIds: { 'cal-mixed': true } },
|
|
{ '@type': 'Event', calendarIds: { 'cal-mixed': true } },
|
|
];
|
|
expect(findTasksOnlyCalendarIds(objects, ['cal-mixed']).size).toBe(0);
|
|
});
|
|
|
|
it('does not flag an empty calendar (no objects)', () => {
|
|
expect(findTasksOnlyCalendarIds([], ['cal-empty']).size).toBe(0);
|
|
const objects: ScannedCalendarObject[] = [{ '@type': 'Event', calendarIds: { 'cal-other': true } }];
|
|
expect(findTasksOnlyCalendarIds(objects, ['cal-empty']).size).toBe(0);
|
|
});
|
|
|
|
it('classifies each calendar independently in a mixed account', () => {
|
|
const objects: ScannedCalendarObject[] = [
|
|
{ '@type': 'Event', calendarIds: { work: true } },
|
|
{ '@type': 'Task', calendarIds: { todos: true } },
|
|
{ percentComplete: 50, calendarIds: { todos: true } },
|
|
];
|
|
const result = findTasksOnlyCalendarIds(objects, ['work', 'todos', 'empty']);
|
|
expect([...result]).toEqual(['todos']);
|
|
});
|
|
|
|
it('handles an object that belongs to several calendars', () => {
|
|
// An event shared into two calendars keeps both off the tasks-only list.
|
|
const objects: ScannedCalendarObject[] = [
|
|
{ '@type': 'Event', calendarIds: { a: true, b: true } },
|
|
{ '@type': 'Task', calendarIds: { b: true } },
|
|
];
|
|
expect(findTasksOnlyCalendarIds(objects, ['a', 'b']).size).toBe(0);
|
|
});
|
|
});
|