Files
time_to_leave/apps/mobile/src/__tests__/screens.test.tsx
T
fegger b240d638ef Translate UI text to English
Translate mobile app UI text to English
2026-05-18 21:50:46 +02:00

299 lines
8.9 KiB
TypeScript

// Tests for UI screens
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import { EventListScreen } from '../screens/EventListScreen';
import { AddEventScreen } from '../screens/AddEventScreen';
import { loadEvents, loadNotificationSettings, loadOriginStation } from '../store/eventStore';
import { calculateCountdown } from '@timetoleave/core';
// Mock the store and utilities
jest.mock('../store/eventStore', () => ({
loadEvents: jest.fn(),
loadOriginStation: jest.fn(),
loadNotificationSettings: jest.fn(),
removeEvent: jest.fn(),
}));
jest.mock('@timetoleave/core', () => ({
...jest.requireActual('@timetoleave/core'),
calculateCountdown: jest.fn(),
}));
jest.mock('../hooks/useColors', () => ({
useColors: () => ({
background: '#000',
card: '#111',
text: '#fff',
subtext: '#aaa',
accent: '#8B5CF6',
border: '#333',
delete: '#ff3b30',
error: '#ff3b30',
overlay: '#111',
}),
}));
jest.mock('../hooks/useDestinationStation', () => ({
useDestinationStation: () => ({
station: { name: 'Ziel Bahnhof', extId: '8103000', lat: 48.2, lng: 16.3 },
loading: false,
error: null,
}),
}));
jest.mock('../hooks/useGeocode', () => ({
useGeocode: () => ({
coords: { lat: 48.21, lng: 16.31, display_name: 'Test Destination' },
loading: false,
error: null,
}),
}));
jest.mock('../hooks/useWalkRoute', () => ({
useWalkRoute: () => ({
walkRoute: null,
loading: false,
error: null,
}),
}));
jest.mock('../hooks/useOriginStationWalk', () => ({
useOriginStationWalk: () => ({
station: { name: 'Mödling Bahnhof', extId: '1231701', lat: 48.085, lng: 16.296 },
walkRoute: { distance: 700, duration: 600, steps: [] },
loading: false,
error: null,
}),
}));
jest.mock('../services/api', () => ({
api: {
findStationByExtId: jest.fn().mockResolvedValue({
name: 'Mödling Bahnhof',
extId: '1231701',
lat: 48.085,
lng: 16.296,
}),
searchJourneys: jest.fn().mockResolvedValue([
{
id: 'journey-1',
sD: new Date('2099-01-01T08:00:00Z'),
rD: new Date('2099-01-01T08:00:00Z'),
sA: new Date('2099-01-01T09:00:00Z'),
rA: new Date('2099-01-01T09:00:00Z'),
delay: 0,
platform: '1',
changes: 0,
trains: ['S1 -> Wien'],
cancelled: false,
},
]),
},
}));
// Mock useFocusEffect so EventListScreen can render without NavigationContainer
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useFocusEffect: (callback: () => void) => {
const React = jest.requireActual('react');
React.useEffect(() => {
callback();
}, [callback]);
},
}));
// --- Mock navigation factories ---
type RootStack = {
EventList: undefined;
EventDetail: { eventId: string };
AddEvent: undefined;
Settings: undefined;
CalendarImport: undefined;
};
function createMockNavigationProp<
S extends Record<string, undefined | Record<string, unknown>>,
T extends keyof S
>(overrides?: Partial<NativeStackNavigationProp<S, T>>): NativeStackNavigationProp<S, T> {
const mocks: Partial<NativeStackNavigationProp<S, T>> = {
navigate: jest.fn(),
dispatch: jest.fn(() => {}),
goBack: jest.fn(),
isFocused: jest.fn(() => true),
setParams: jest.fn(),
setOptions: jest.fn(),
reset: jest.fn(),
pop: jest.fn(),
preload: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
canGoBack: jest.fn(() => false),
...overrides,
};
return mocks as NativeStackNavigationProp<S, T>;
}
const mockRouteEventList = { name: 'EventList' as const, params: undefined } as unknown as RouteProp<RootStack, 'EventList'>;
const mockRouteAddEvent = { name: 'AddEvent' as const, params: undefined } as unknown as RouteProp<RootStack, 'AddEvent'>;
// --- End mock factories ---
describe('EventListScreen', () => {
beforeEach(() => {
jest.clearAllMocks();
(loadOriginStation as jest.Mock).mockResolvedValue({
name: 'Mödling Bahnhof',
extId: '1231701',
lat: 48.08,
lng: 16.29,
});
(loadNotificationSettings as jest.Mock).mockResolvedValue({
bufferMinutes: 30,
enabled: true,
arrivalBufferMinutes: 5,
showWalkingOption: true,
showBikeOption: true,
});
});
it('should render empty state when no events', async () => {
(loadEvents as jest.Mock).mockResolvedValue([]);
const { getByText } = render(
<EventListScreen navigation={createMockNavigationProp<RootStack, 'EventList'>()} route={mockRouteEventList} />
);
await waitFor(() => {
expect(getByText('No upcoming events')).toBeTruthy();
});
});
it('should render events when they exist', async () => {
const mockEvents = [
{
id: 'test-1',
title: 'Test Event',
destination: 'Test Destination',
eventTime: new Date('2099-01-01T10:00:00Z'),
source: 'manual',
}
];
(loadEvents as jest.Mock).mockResolvedValue(mockEvents);
(calculateCountdown as jest.Mock).mockReturnValue({
label: '30min',
color: 'green',
urgent: false
});
const { getByText } = render(
<EventListScreen navigation={createMockNavigationProp<RootStack, 'EventList'>()} route={mockRouteEventList} />
);
await waitFor(() => {
expect(getByText('Test Event')).toBeTruthy();
expect(getByText('Test Destination')).toBeTruthy();
});
await waitFor(() => {
expect(getByText('Leave in')).toBeTruthy();
expect(getByText('S1 -> Wien')).toBeTruthy();
expect(getByText('Incl. walk to station: 10 min')).toBeTruthy();
});
});
it('should handle refresh correctly', async () => {
const mockEvents = [
{
id: 'test-1',
title: 'Test Event',
destination: 'Test Destination',
eventTime: new Date('2099-01-01T10:00:00Z'),
source: 'manual',
}
];
(loadEvents as jest.Mock).mockResolvedValue(mockEvents);
(calculateCountdown as jest.Mock).mockReturnValue({
label: '30min',
color: 'green',
urgent: false
});
const { getByText } = render(
<EventListScreen navigation={createMockNavigationProp<RootStack, 'EventList'>()} route={mockRouteEventList} />
);
await waitFor(() => {
expect(getByText('Test Event')).toBeTruthy();
});
});
});
describe('AddEventScreen', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render form correctly', () => {
const { getByPlaceholderText, getByText } = render(
<AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
);
expect(getByPlaceholderText('e.g. Team Meeting')).toBeTruthy();
expect(getByPlaceholderText('e.g. Technikum Wien')).toBeTruthy();
expect(getByPlaceholderText('YYYY-MM-DD')).toBeTruthy();
expect(getByPlaceholderText('HH:MM')).toBeTruthy();
expect(getByText('Save')).toBeTruthy();
expect(getByText('Cancel')).toBeTruthy();
});
it('should show validation errors', () => {
const { getByText } = render(
<AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
);
// Try to save without filling form
const saveButton = getByText('Save');
fireEvent.press(saveButton);
// Should show error text
expect(getByText('Title required')).toBeTruthy();
});
it('should validate date format', () => {
const { getByPlaceholderText, getByText } = render(
<AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
);
// Fill in all required fields except date format is invalid
fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), 'invalid-date');
fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Save');
fireEvent.press(saveButton);
expect(getByText('Invalid date')).toBeTruthy();
});
it('should validate future date', () => {
const { getByPlaceholderText, getByText } = render(
<AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
);
// Fill in all required fields with a past date
fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), '2020-01-01');
fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Save');
fireEvent.press(saveButton);
expect(getByText('Date must be in the future')).toBeTruthy();
});
});