mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
211 lines
8.1 KiB
TypeScript
211 lines
8.1 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { useTaskStore } from '../task-store';
|
|
import { clearAllHooks, taskHooks } from '@/lib/plugin-hooks';
|
|
import type { CalendarTask } from '@/lib/jmap/types';
|
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
|
|
// Plugins hear about loaded, created, changed, deleted and checked-off tasks
|
|
// and filter changes; the before-hooks run ahead of the server call and can
|
|
// stop the change.
|
|
|
|
function makeTask(overrides: Partial<CalendarTask> = {}): CalendarTask {
|
|
return {
|
|
id: 't1',
|
|
calendarIds: { 'cal-1': true },
|
|
'@type': 'Task',
|
|
uid: 'task-uid-1',
|
|
title: 'Write report',
|
|
description: '',
|
|
due: '2026-09-08T10:00:00',
|
|
progress: 'needs-action',
|
|
priority: 0,
|
|
keywords: null,
|
|
...overrides,
|
|
} as CalendarTask;
|
|
}
|
|
|
|
let client: IJMAPClient;
|
|
|
|
beforeEach(() => {
|
|
clearAllHooks();
|
|
useTaskStore.setState({ tasks: [], selectedTaskId: null, filter: 'all', showCompleted: false, isLoading: false, error: null });
|
|
client = {
|
|
getCalendarTasks: vi.fn().mockResolvedValue([makeTask()]),
|
|
createCalendarTask: vi.fn(async (t: Partial<CalendarTask>) => makeTask({ ...t, id: 'created' })),
|
|
updateCalendarTask: vi.fn().mockResolvedValue(undefined),
|
|
deleteCalendarTask: vi.fn().mockResolvedValue(undefined),
|
|
} as never;
|
|
});
|
|
|
|
describe('onTasksLoaded', () => {
|
|
it('reports the fetched tasks as views', async () => {
|
|
const handler = vi.fn();
|
|
taskHooks.onTasksLoaded.register('t', handler);
|
|
await useTaskStore.getState().fetchTasks(client);
|
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
expect(handler.mock.calls[0][0]).toEqual([
|
|
expect.objectContaining({ id: 't1', title: 'Write report', isComplete: false, calendarId: 'cal-1' }),
|
|
]);
|
|
});
|
|
|
|
it('stays quiet when loading failed', async () => {
|
|
vi.mocked(client.getCalendarTasks).mockRejectedValue(new Error('boom'));
|
|
const handler = vi.fn();
|
|
taskHooks.onTasksLoaded.register('t', handler);
|
|
await useTaskStore.getState().fetchTasks(client);
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('create, update and delete', () => {
|
|
it('asks before creating and reports the created task', async () => {
|
|
const before = vi.fn((view) => {
|
|
expect(client.createCalendarTask).not.toHaveBeenCalled();
|
|
expect(view).toMatchObject({ title: 'New task' });
|
|
return true;
|
|
});
|
|
taskHooks.onBeforeTaskCreate.register('t', before);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskCreate.register('t', after);
|
|
|
|
await useTaskStore.getState().createTask(client, { title: 'New task', calendarIds: { 'cal-1': true } });
|
|
|
|
expect(before).toHaveBeenCalledTimes(1);
|
|
expect(after).toHaveBeenCalledTimes(1);
|
|
expect(after.mock.calls[0][0]).toMatchObject({ id: 'created', title: 'New task' });
|
|
});
|
|
|
|
it('creates nothing when a plugin says no', async () => {
|
|
taskHooks.onBeforeTaskCreate.register('t', () => false);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskCreate.register('t', after);
|
|
|
|
const created = await useTaskStore.getState().createTask(client, { title: 'New task' });
|
|
|
|
expect(created).toBeNull();
|
|
expect(client.createCalendarTask).not.toHaveBeenCalled();
|
|
expect(useTaskStore.getState().tasks).toEqual([]);
|
|
expect(after).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('asks before changing and reports the changed task', async () => {
|
|
useTaskStore.setState({ tasks: [makeTask()] });
|
|
const before = vi.fn(() => {
|
|
expect(client.updateCalendarTask).not.toHaveBeenCalled();
|
|
return true;
|
|
});
|
|
taskHooks.onBeforeTaskUpdate.register('t', before);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskUpdate.register('t', after);
|
|
|
|
expect(await useTaskStore.getState().updateTask(client, 't1', { title: 'Renamed' })).toBe(true);
|
|
|
|
expect(before).toHaveBeenCalledWith({ taskId: 't1', patch: { title: 'Renamed' } });
|
|
expect(after).toHaveBeenCalledTimes(1);
|
|
expect(after.mock.calls[0][0]).toMatchObject({ id: 't1', title: 'Renamed' });
|
|
});
|
|
|
|
it('leaves the task alone when a plugin says no', async () => {
|
|
useTaskStore.setState({ tasks: [makeTask()] });
|
|
taskHooks.onBeforeTaskUpdate.register('t', () => false);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskUpdate.register('t', after);
|
|
|
|
expect(await useTaskStore.getState().updateTask(client, 't1', { title: 'Renamed' })).toBe(false);
|
|
|
|
expect(client.updateCalendarTask).not.toHaveBeenCalled();
|
|
expect(useTaskStore.getState().tasks[0].title).toBe('Write report');
|
|
expect(after).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not report a change the server refused', async () => {
|
|
useTaskStore.setState({ tasks: [makeTask()] });
|
|
vi.mocked(client.updateCalendarTask).mockRejectedValue(new Error('boom'));
|
|
const after = vi.fn();
|
|
taskHooks.onTaskUpdate.register('t', after);
|
|
await expect(useTaskStore.getState().updateTask(client, 't1', { title: 'X' })).rejects.toThrow('boom');
|
|
expect(after).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('asks before deleting and reports the deleted task with its title', async () => {
|
|
useTaskStore.setState({ tasks: [makeTask()] });
|
|
const before = vi.fn(() => {
|
|
expect(client.deleteCalendarTask).not.toHaveBeenCalled();
|
|
return true;
|
|
});
|
|
taskHooks.onBeforeTaskDelete.register('t', before);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskDelete.register('t', after);
|
|
|
|
expect(await useTaskStore.getState().deleteTask(client, 't1')).toBe(true);
|
|
|
|
expect(before).toHaveBeenCalledWith({ taskId: 't1', title: 'Write report' });
|
|
expect(after).toHaveBeenCalledWith({ taskId: 't1', title: 'Write report' });
|
|
});
|
|
|
|
it('keeps the task when a plugin says no, and reports no failed delete', async () => {
|
|
useTaskStore.setState({ tasks: [makeTask()] });
|
|
taskHooks.onBeforeTaskDelete.register('t', () => false);
|
|
const after = vi.fn();
|
|
taskHooks.onTaskDelete.register('t', after);
|
|
|
|
expect(await useTaskStore.getState().deleteTask(client, 't1')).toBe(false);
|
|
expect(client.deleteCalendarTask).not.toHaveBeenCalled();
|
|
expect(useTaskStore.getState().tasks).toHaveLength(1);
|
|
|
|
clearAllHooks();
|
|
taskHooks.onTaskDelete.register('t', after);
|
|
vi.mocked(client.deleteCalendarTask).mockRejectedValue(new Error('boom'));
|
|
await expect(useTaskStore.getState().deleteTask(client, 't1')).rejects.toThrow('boom');
|
|
expect(after).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('onTaskToggleComplete', () => {
|
|
it('reports the new state after the server took it, and not as an update', async () => {
|
|
const task = makeTask();
|
|
useTaskStore.setState({ tasks: [task] });
|
|
const toggled = vi.fn((_payload: unknown) => {
|
|
expect(client.updateCalendarTask).toHaveBeenCalled();
|
|
});
|
|
taskHooks.onTaskToggleComplete.register('t', toggled);
|
|
const updated = vi.fn();
|
|
taskHooks.onTaskUpdate.register('t', updated);
|
|
|
|
await useTaskStore.getState().toggleTaskComplete(client, task);
|
|
await useTaskStore.getState().toggleTaskComplete(client, { ...task, progress: 'completed' });
|
|
|
|
expect(toggled.mock.calls.map((c) => c[0])).toEqual([
|
|
{ taskId: 't1', complete: true },
|
|
{ taskId: 't1', complete: false },
|
|
]);
|
|
expect(updated).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('stays quiet when the server refused', async () => {
|
|
const task = makeTask();
|
|
vi.mocked(client.updateCalendarTask).mockRejectedValue(new Error('boom'));
|
|
const toggled = vi.fn();
|
|
taskHooks.onTaskToggleComplete.register('t', toggled);
|
|
await expect(useTaskStore.getState().toggleTaskComplete(client, task)).rejects.toThrow('boom');
|
|
expect(toggled).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('onTaskFilterChange', () => {
|
|
it('reports filter and show-completed changes, but not unchanged values', () => {
|
|
const handler = vi.fn();
|
|
taskHooks.onTaskFilterChange.register('t', handler);
|
|
useTaskStore.getState().setFilter('all');
|
|
useTaskStore.getState().setShowCompleted(false);
|
|
expect(handler).not.toHaveBeenCalled();
|
|
|
|
useTaskStore.getState().setFilter('overdue');
|
|
useTaskStore.getState().setShowCompleted(true);
|
|
expect(handler.mock.calls.map((c) => c[0])).toEqual([
|
|
{ filter: 'overdue', showCompleted: false },
|
|
{ filter: 'overdue', showCompleted: true },
|
|
]);
|
|
});
|
|
});
|