Files
fotospiel-app/resources/js/admin/mobile/__tests__/EventsPage.test.tsx
Codex Agent 9d8f01d294
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Refresh event overview list UI
2026-01-20 13:21:39 +01:00

302 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen } from '@testing-library/react';
import * as api from '../../api';
const navigateMock = vi.fn();
const authState = {
user: { role: 'tenant_admin' },
};
vi.mock('react-router-dom', () => ({
useNavigate: () => navigateMock,
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'events.list.title': 'Your events',
'events.list.subtitle': 'Plan memorable moments. Manage everything around your events here.',
'events.list.overview.title': 'Overview',
'events.list.overview.empty': 'No events yet create your first one to get started.',
'events.list.filters.all': 'All',
'events.list.filters.upcoming': 'Upcoming',
'events.list.filters.draft': 'Draft',
'events.list.filters.past': 'Past',
'events.list.actions.create': 'New event',
'events.list.actions.open': 'Open event',
'events.list.empty.filtered': 'No events match this filter.',
'events.list.empty.filteredHint': 'Try a different status or clear your search.',
'events.list.stats.photos': 'Photos',
'events.list.stats.guests': 'Guests',
'events.list.stats.tasks': 'Photo tasks',
'events.workspace.fields.status': 'Status',
'events.detail.pickEvent': 'Select event',
'events.detail.dateTbd': 'Date tbd',
'events.detail.locationPlaceholder': 'Location',
'events.placeholders.untitled': 'Untitled event',
'events.errors.loadFailed': 'Event konnte nicht geladen werden.',
};
return translations[key] ?? key;
},
i18n: {
language: 'en',
},
}),
}));
vi.mock('../../api', () => ({
getEvents: vi.fn().mockResolvedValue([
{
id: 1,
name: 'Demo Event',
slug: 'demo-event',
event_date: '2026-02-19',
settings: {},
},
{
id: 2,
name: { en: 'Summer Gala' },
slug: 'summer-gala',
event_date: '2026-06-01',
settings: {},
},
]),
getTenantPackagesOverview: vi.fn().mockResolvedValue({
packages: [
{
id: 1,
package_id: 1,
package_name: 'Standard',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
},
],
activePackage: {
id: 1,
package_id: 1,
package_name: 'Standard',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
},
}),
}));
vi.mock('../../auth/tokens', () => ({
isAuthError: () => false,
}));
vi.mock('../../auth/context', () => ({
useAuth: () => authState,
}));
vi.mock('../../lib/apiError', () => ({
getApiErrorMessage: () => 'error',
}));
vi.mock('../../lib/eventFilters', () => ({
buildEventStatusCounts: () => ({ all: 1, upcoming: 1, draft: 0, past: 0 }),
filterEventsByStatus: (events: any[]) => events,
resolveEventStatusKey: () => 'upcoming',
}));
vi.mock('../../lib/eventListStats', () => ({
buildEventListStats: () => ({ photos: 2, guests: 3, tasks: 4 }),
}));
vi.mock('../hooks/useBackNavigation', () => ({
useBackNavigation: () => undefined,
}));
vi.mock('../components/MobileShell', () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Primitives', () => ({
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
FloatingActionButton: ({ label }: { label: string }) => <div>{label}</div>,
SkeletonCard: () => <div>Loading...</div>,
}));
vi.mock('../components/FormControls', () => ({
MobileInput: ({ compact: _compact, ...props }: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
}));
vi.mock('@tamagui/card', () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/scroll-view', () => ({
ScrollView: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{children}
</button>
),
}));
vi.mock('@tamagui/toggle-group', () => ({
ToggleGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/separator', () => ({
Separator: () => <div />,
}));
vi.mock('@tamagui/list-item', () => ({
ListItem: ({
title,
iconAfter,
onPress,
}: {
title?: React.ReactNode;
iconAfter?: React.ReactNode;
onPress?: () => void;
}) => (
<div onClick={onPress}>
{title}
{iconAfter}
</div>
),
}));
vi.mock('@tamagui/group', () => ({
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('../theme', () => ({
useAdminTheme: () => ({
text: '#111827',
muted: '#6b7280',
subtle: '#94a3b8',
border: '#e5e7eb',
primary: '#ff5a5f',
danger: '#dc2626',
surface: '#ffffff',
surfaceMuted: '#f9fafb',
accentSoft: '#eef2ff',
accent: '#6366f1',
shadow: 'rgba(15,23,42,0.12)',
}),
}));
import MobileEventsPage from '../EventsPage';
describe('MobileEventsPage', () => {
it('renders filters and event list', async () => {
render(<MobileEventsPage />);
expect(await screen.findByText('Overview')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Demo Event')).toBeInTheDocument();
});
it('hides create actions for members', async () => {
authState.user = { role: 'member' };
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
authState.user = { role: 'tenant_admin' };
});
it('filters events by search query', async () => {
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.getByText('Summer Gala')).toBeInTheDocument();
await act(async () => {
fireEvent.change(screen.getByPlaceholderText('Select event'), { target: { value: 'Summer' } });
});
expect(screen.getByText('Summer Gala')).toBeInTheDocument();
expect(screen.queryByText('Demo Event')).not.toBeInTheDocument();
});
it('shows the create button when an active package has remaining events', async () => {
render(<MobileEventsPage />);
expect(await screen.findByText('New event')).toBeInTheDocument();
});
it('hides the create button when no remaining events are available', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 5,
remaining_events: 0,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
],
activePackage: {
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 5,
remaining_events: 0,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
});
});