9e461aab1a
Switch mobile app to dark theme and fix calendar filtering - Default to dark theme in mobile app, matching web app style - Filter native calendar events by location to exclude empty ones - Add batch edit panel for destinations on web calendar - Add edit support to AddEventModal - Update notification trigger input type export
159 lines
6.1 KiB
TypeScript
159 lines
6.1 KiB
TypeScript
import * as Calendar from 'expo-calendar';
|
|
import { ensureCalendarPermission, fetchNativeEvents } from '../services/calendar';
|
|
|
|
// Mock expo-calendar
|
|
jest.mock('expo-calendar', () => ({
|
|
requestCalendarPermissionsAsync: jest.fn(),
|
|
isAvailableAsync: jest.fn(),
|
|
getCalendarsAsync: jest.fn(),
|
|
getEventsAsync: jest.fn(),
|
|
EntityTypes: {
|
|
EVENTS: 'EVENTS',
|
|
},
|
|
}));
|
|
|
|
const mockCalendar = Calendar as jest.Mocked<typeof Calendar>;
|
|
|
|
describe('calendar service', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('ensureCalendarPermission', () => {
|
|
it('returns true when permission granted and calendar available', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
|
|
|
const result = await ensureCalendarPermission();
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('returns false when permission denied', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true });
|
|
|
|
const result = await ensureCalendarPermission();
|
|
expect(result).toBe(false);
|
|
expect(mockCalendar.isAvailableAsync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns false when calendar not available', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(false);
|
|
|
|
const result = await ensureCalendarPermission();
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('fetchNativeEvents', () => {
|
|
it('returns empty array when no permission', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true });
|
|
|
|
const result = await fetchNativeEvents(new Date(), new Date());
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array when no calendars', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
|
mockCalendar.getCalendarsAsync.mockResolvedValue([]);
|
|
|
|
const result = await fetchNativeEvents(new Date(), new Date());
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns mapped events from native calendar', async () => {
|
|
const startDate = new Date('2025-01-01');
|
|
const endDate = new Date('2025-01-31');
|
|
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
|
mockCalendar.getCalendarsAsync.mockResolvedValue([
|
|
{ id: 'cal1', title: 'Work' },
|
|
{ id: 'cal2', title: 'Personal' },
|
|
] as Calendar.Calendar[]);
|
|
mockCalendar.getEventsAsync.mockResolvedValue([
|
|
{
|
|
id: 'evt1',
|
|
calendarId: 'cal1',
|
|
title: 'Team Meeting',
|
|
location: 'Berlin',
|
|
startDate: new Date('2025-01-15T10:00:00'),
|
|
},
|
|
{
|
|
id: 'evt2',
|
|
calendarId: 'cal2',
|
|
title: 'Dentist',
|
|
location: null,
|
|
startDate: new Date('2025-01-20T14:00:00'),
|
|
},
|
|
] as Calendar.Event[]);
|
|
|
|
const result = await fetchNativeEvents(startDate, endDate);
|
|
|
|
// Only the event with a location is returned; events without a location are filtered out
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual({
|
|
id: 'evt1',
|
|
title: 'Team Meeting',
|
|
destination: 'Berlin',
|
|
eventTime: new Date('2025-01-15T10:00:00'),
|
|
source: 'native:cal1',
|
|
});
|
|
|
|
expect(mockCalendar.getEventsAsync).toHaveBeenCalledWith(
|
|
['cal1', 'cal2'],
|
|
startDate,
|
|
endDate,
|
|
);
|
|
});
|
|
|
|
it('filters out events with no location', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
|
mockCalendar.getCalendarsAsync.mockResolvedValue([{ id: 'cal1', title: 'Default' }] as Calendar.Calendar[]);
|
|
mockCalendar.getEventsAsync.mockResolvedValue([
|
|
{
|
|
id: 'evt1',
|
|
calendarId: 'cal1',
|
|
title: 'No Location Event',
|
|
location: null,
|
|
startDate: new Date('2025-01-15T10:00:00'),
|
|
},
|
|
{
|
|
id: 'evt2',
|
|
calendarId: 'cal1',
|
|
title: 'Empty Location Event',
|
|
location: ' ',
|
|
startDate: new Date('2025-01-16T10:00:00'),
|
|
},
|
|
] as Calendar.Event[]);
|
|
|
|
const result = await fetchNativeEvents(new Date(), new Date());
|
|
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it('handles events with missing title', async () => {
|
|
mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true });
|
|
mockCalendar.isAvailableAsync.mockResolvedValue(true);
|
|
mockCalendar.getCalendarsAsync.mockResolvedValue([{ id: 'cal1', title: 'Default' }] as Calendar.Calendar[]);
|
|
mockCalendar.getEventsAsync.mockResolvedValue([
|
|
{
|
|
id: 'evt1',
|
|
calendarId: 'cal1',
|
|
title: null as unknown as string,
|
|
location: 'Wien Hbf',
|
|
startDate: null as unknown as string | Date,
|
|
},
|
|
] as Calendar.Event[]);
|
|
|
|
const result = await fetchNativeEvents(new Date(), new Date());
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Untitled Event');
|
|
expect(result[0].destination).toBe('Wien Hbf');
|
|
});
|
|
});
|
|
});
|