mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
Saving in the tasks sheet ran `onCreate`/`onUpdate` in a `try/finally` without a `catch`, so a refused create or update became an unhandled rejection: the spinner stopped, nothing was shown, and the user could not tell whether the task was saved. The editor state and the save logic move to `src/lib/task-editor.ts`. `submitTaskEditor` returns the server's reason instead of throwing; the sheet shows it in an alert and leaves the editor untouched so the input can be fixed or saved again. It is cleared only after a successful save.
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
vi.mock('../../stores/settings-store', () => ({
|
|
useSettingsStore: { getState: () => ({ calendarTimeZone: 'Europe/Berlin' }) },
|
|
}));
|
|
|
|
import {
|
|
buildTaskEditorChanges,
|
|
emptyTaskEditor,
|
|
levelToPriority,
|
|
priorityToLevel,
|
|
submitTaskEditor,
|
|
type TaskEditorState,
|
|
} from '../task-editor';
|
|
import type { CalendarEvent } from '../../api/types';
|
|
|
|
function editor(partial: Partial<TaskEditorState>): TaskEditorState {
|
|
return { ...emptyTaskEditor('cal-1'), title: 'Buy milk', ...partial };
|
|
}
|
|
|
|
const existing = {
|
|
id: 't1',
|
|
title: 'Old',
|
|
calendarIds: { 'cal-1': true },
|
|
} as unknown as CalendarEvent;
|
|
|
|
describe('priority mapping', () => {
|
|
it('maps RFC 8984 priorities onto the editor levels and back', () => {
|
|
expect(priorityToLevel(undefined)).toBe('none');
|
|
expect(priorityToLevel(0)).toBe('none');
|
|
expect(priorityToLevel(3)).toBe('high');
|
|
expect(priorityToLevel(5)).toBe('medium');
|
|
expect(priorityToLevel(7)).toBe('low');
|
|
expect(levelToPriority('high')).toBe(1);
|
|
expect(levelToPriority('none')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('buildTaskEditorChanges', () => {
|
|
it('labels a timed due with the calendar zone and a date-only due with none', () => {
|
|
const timed = buildTaskEditorChanges(editor({ due: new Date(2026, 8, 24, 17, 30), withTime: true }));
|
|
expect(timed).toMatchObject({
|
|
title: 'Buy milk',
|
|
due: '2026-09-24T17:30:00',
|
|
showWithoutTime: false,
|
|
timeZone: 'Europe/Berlin',
|
|
});
|
|
const dateOnly = buildTaskEditorChanges(editor({ due: new Date(2026, 8, 24, 17, 30) }));
|
|
expect(dateOnly).toMatchObject({ due: '2026-09-24T00:00:00', showWithoutTime: true, timeZone: null });
|
|
});
|
|
|
|
it('clears the due of an edited task and sends a changed calendar only', () => {
|
|
expect(buildTaskEditorChanges(editor({ id: 't1' }), existing)).toMatchObject({ due: null });
|
|
expect(buildTaskEditorChanges(editor({ id: 't1' }), existing).calendarIds).toBeUndefined();
|
|
expect(buildTaskEditorChanges(editor({ id: 't1', calendarId: 'cal-2' }), existing).calendarIds)
|
|
.toEqual({ 'cal-2': true });
|
|
});
|
|
});
|
|
|
|
describe('submitTaskEditor', () => {
|
|
it('creates a new task as needs-action in the chosen calendar', async () => {
|
|
const onCreate = vi.fn(async () => undefined);
|
|
const result = await submitTaskEditor(editor({ calendarId: 'cal-2' }), [], { onCreate });
|
|
expect(result).toEqual({ ok: true });
|
|
expect(onCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({ title: 'Buy milk', progress: 'needs-action' }),
|
|
'cal-2',
|
|
);
|
|
});
|
|
|
|
it('reports a refused create with the server reason instead of throwing', async () => {
|
|
const onCreate = vi.fn(async () => {
|
|
throw new Error('forbidden: calendar is read-only');
|
|
});
|
|
const result = await submitTaskEditor(editor({}), [], { onCreate });
|
|
expect(result).toEqual({ ok: false, message: 'forbidden: calendar is read-only' });
|
|
});
|
|
|
|
it('reports a refused update too', async () => {
|
|
const onUpdate = vi.fn(async () => {
|
|
throw new Error('invalidProperties');
|
|
});
|
|
const result = await submitTaskEditor(editor({ id: 't1' }), [existing], {
|
|
onCreate: vi.fn(),
|
|
onUpdate,
|
|
});
|
|
expect(onUpdate).toHaveBeenCalledWith('t1', expect.objectContaining({ title: 'Buy milk' }));
|
|
expect(result).toEqual({ ok: false, message: 'invalidProperties' });
|
|
});
|
|
|
|
it('reports a failure without a message', async () => {
|
|
const result = await submitTaskEditor(editor({}), [], {
|
|
onCreate: () => Promise.reject('nope'),
|
|
});
|
|
expect(result).toEqual({ ok: false, message: undefined });
|
|
});
|
|
});
|