b240d638ef
Translate mobile app UI text to English
299 lines
8.9 KiB
TypeScript
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();
|
|
});
|
|
});
|