import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { render, screen, waitFor, within } from '@testing-library/react'; import { ADMIN_EVENTS_PATH } from '../../constants'; const fixtures = vi.hoisted(() => ({ event: { id: 1, name: 'Demo Wedding', slug: 'demo-event', event_date: '2026-02-19', status: 'published' as const, settings: { location: 'Berlin' }, tasks_count: 4, photo_count: 12, active_invites_count: 3, total_invites_count: 5, member_permissions: ['photos:moderate', 'tasks:manage', 'join-tokens:manage'], }, activePackage: { id: 1, package_id: 1, package_name: 'Standard', package_type: 'reseller', included_package_slug: null, active: true, used_events: 2, remaining_events: 3, price: null, currency: null, purchased_at: null, expires_at: null, package_limits: null, branding_allowed: true, watermark_allowed: true, features: [], }, })); const navigateMock = vi.fn(); const authState = { status: 'authenticated', user: { role: 'tenant_admin' }, }; const paramsState = { slug: fixtures.event.slug as string | undefined, }; const eventContext = { events: [fixtures.event], activeEvent: fixtures.event, hasEvents: true, hasMultipleEvents: false, isLoading: false, }; vi.mock('react-router-dom', () => ({ useNavigate: () => navigateMock, useLocation: () => ({ search: '', pathname: '/event-admin/mobile/dashboard' }), useParams: () => paramsState, })); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback?: string | Record, options?: Record) => { let text = key; let resolvedOptions = options; if (typeof fallback === 'string') { text = fallback; } else if (fallback && typeof fallback === 'object') { resolvedOptions = fallback; if (typeof fallback.defaultValue === 'string') { text = fallback.defaultValue; } } if (!resolvedOptions || typeof text !== 'string') { return text; } return text.replace(/\{\{(\w+)\}\}/g, (_, token) => String(resolvedOptions?.[token] ?? '')); }, i18n: { language: 'de', }, }), initReactI18next: { type: '3rdParty', init: () => undefined, }, })); vi.mock('@tanstack/react-query', () => ({ useQuery: ({ queryKey }: { queryKey: unknown }) => { const keyParts = Array.isArray(queryKey) ? queryKey : [queryKey]; const key = keyParts.map(String).join(':'); if (key.includes('packages-overview')) { return { data: { packages: [], activePackage: fixtures.activePackage }, isLoading: false, isError: false }; } if (key.includes('onboarding') && key.includes('status')) { return { data: { steps: { summary_seen_package_id: fixtures.activePackage.id } }, isLoading: false }; } if (key.includes('dashboard') && key.includes('events')) { return { data: [fixtures.event], isLoading: false }; } if (key.includes('dashboard') && key.includes('stats')) { return { data: null, isLoading: false }; } return { data: null, isLoading: false, isError: false }; }, })); vi.mock('../../context/EventContext', () => ({ useEventContext: () => ({ ...eventContext, selectEvent: vi.fn(), }), })); vi.mock('../../auth/context', () => ({ useAuth: () => authState, })); vi.mock('../hooks/useInstallPrompt', () => ({ useInstallPrompt: () => ({ isInstalled: true, canInstall: false, isIos: false, promptInstall: vi.fn() }), })); vi.mock('../hooks/useAdminPushSubscription', () => ({ useAdminPushSubscription: () => ({ supported: true, subscribed: true, permission: 'granted', loading: false, enable: vi.fn() }), })); vi.mock('../hooks/useDevicePermissions', () => ({ useDevicePermissions: () => ({ storage: 'unavailable', loading: false, requestPersistentStorage: vi.fn() }), })); vi.mock('../lib/mobileTour', () => ({ getTourSeen: () => true, resolveTourStepKeys: () => [], setTourSeen: vi.fn(), })); vi.mock('../components/MobileShell', () => ({ MobileShell: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('../components/Sheet', () => ({ MobileSheet: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('../components/Primitives', () => ({ MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, CTAButton: ({ label }: { label: string }) => , KpiStrip: ({ items }: { items: Array<{ label: string; value: string | number }> }) => (
{items.map((item) => ( {item.label} ))}
), KpiTile: ({ label, value }: { label: string; value: string | number }) => (
{label} {value}
), ActionTile: ({ label, note }: { label: string; note?: string }) => (
{label} {note ? {note} : null}
), PillBadge: ({ children }: { children: React.ReactNode }) =>
{children}
, SkeletonCard: () =>
Loading...
, })); vi.mock('@tamagui/card', () => ({ Card: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('@tamagui/progress', () => ({ Progress: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { Indicator: ({ children }: { children?: React.ReactNode }) =>
{children}
, }), })); vi.mock('@tamagui/group', () => ({ XGroup: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { Item: ({ children }: { children: React.ReactNode }) =>
{children}
, }), YGroup: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { Item: ({ children }: { children: React.ReactNode }) =>
{children}
, }), })); vi.mock('@tamagui/stacks', () => ({ YStack: ({ children }: { children: React.ReactNode }) =>
{children}
, XStack: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('@tamagui/button', () => ({ Button: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => ( ), })); vi.mock('@tamagui/list-item', () => ({ ListItem: ({ title, subTitle, iconAfter, onPress, }: { title?: React.ReactNode; subTitle?: React.ReactNode; iconAfter?: React.ReactNode; onPress?: () => void; }) => ( ), })); vi.mock('tamagui', () => ({ Separator: ({ children }: { children?: React.ReactNode }) =>
{children}
, })); vi.mock('@tamagui/text', () => ({ SizableText: ({ children }: { children: React.ReactNode }) => {children}, })); vi.mock('@tamagui/switch', () => ({ Switch: Object.assign( ({ children, checked, onCheckedChange, }: { children: React.ReactNode; checked?: boolean; onCheckedChange?: (checked: boolean) => void; }) => ( ), { Thumb: ({ children }: { children?: React.ReactNode }) => {children}, }, ), })); vi.mock('@tamagui/react-native-web-lite', () => ({ Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => ( ), })); vi.mock('../theme', () => ({ ADMIN_ACTION_COLORS: { settings: '#10b981', tasks: '#f59e0b', qr: '#6366f1', images: '#ec4899', liveShow: '#f97316', liveShowSettings: '#38bdf8', guests: '#22c55e', guestMessages: '#f97316', branding: '#0ea5e9', photobooth: '#f43f5e', recap: '#94a3b8', analytics: '#22c55e', }, ADMIN_MOTION: { tileStaggerMs: 0, }, useAdminTheme: () => ({ textStrong: '#0f172a', text: '#0f172a', textMuted: '#94a3b8', accent: '#7c3aed', muted: '#64748b', border: '#e2e8f0', surface: '#ffffff', accentSoft: '#eef2ff', primary: '#ff5a5f', surfaceMuted: '#f8fafc', shadow: 'rgba(15,23,42,0.12)', success: '#16a34a', successText: '#166534', dangerBg: '#fee2e2', dangerText: '#b91c1c', warningBg: '#fef9c3', warningBorder: '#fef08a', warningText: '#92400e', }), })); vi.mock('../../lib/events', () => ({ resolveEventDisplayName: () => fixtures.event.name, resolveEngagementMode: () => 'full', isBrandingAllowed: () => true, formatEventDate: () => '19. Feb. 2026', })); vi.mock('../eventDate', () => ({ isPastEvent: () => false, })); import MobileDashboardPage from '../DashboardPage'; describe('MobileDashboardPage', () => { afterEach(() => { eventContext.events = [fixtures.event]; eventContext.activeEvent = fixtures.event; eventContext.hasEvents = true; eventContext.hasMultipleEvents = false; paramsState.slug = fixtures.event.slug; fixtures.activePackage.package_type = 'reseller'; fixtures.activePackage.remaining_events = 3; fixtures.event.tasks_count = 4; fixtures.event.engagement_mode = undefined; fixtures.event.settings = { location: 'Berlin' }; navigateMock.mockClear(); window.sessionStorage.clear(); }); it('redirects to the event selector when no event is active', async () => { eventContext.activeEvent = null as unknown as typeof fixtures.event; eventContext.events = [fixtures.event]; eventContext.hasEvents = true; paramsState.slug = undefined; render(); await waitFor(() => { expect(navigateMock).toHaveBeenCalledWith(ADMIN_EVENTS_PATH, { replace: true }); }); }); it('shows the next-step CTA when tasks are missing', () => { fixtures.event.tasks_count = 0; fixtures.event.engagement_mode = 'tasks'; window.sessionStorage.setItem(`tasksDecisionPrompt:${fixtures.event.id}`, 'pending'); render(); expect(screen.getByText('Fotoaufgaben hinzufügen')).toBeInTheDocument(); }); it('does not redirect endcustomer packages without remaining event quota', () => { eventContext.events = []; eventContext.activeEvent = null as unknown as typeof fixtures.event; eventContext.hasEvents = false; eventContext.hasMultipleEvents = false; fixtures.activePackage.package_type = 'endcustomer'; fixtures.activePackage.remaining_events = 0; render(); expect(navigateMock).not.toHaveBeenCalledWith('/event-admin/mobile/billing#packages', { replace: true }); }); it('shows the activity pulse strip', () => { render(); const strip = screen.getByTestId('kpi-strip'); expect(within(strip).getAllByText('Photos').length).toBeGreaterThan(0); expect(within(strip).getAllByText('Guests').length).toBeGreaterThan(0); expect(within(strip).getAllByText('Pending').length).toBeGreaterThan(0); expect(within(strip).getAllByText('Live Show approved').length).toBeGreaterThan(0); }); it('replaces pending with likes when auto-approval is enabled', () => { fixtures.event.settings = { location: 'Berlin', guest_upload_visibility: 'immediate' }; render(); const strip = screen.getByTestId('kpi-strip'); expect(within(strip).queryByText('Pending')).not.toBeInTheDocument(); expect(within(strip).getAllByText('Likes total').length).toBeGreaterThan(0); }); it('shows shortcut sections for members', () => { authState.user = { role: 'member' }; render(); expect(screen.getByText('Experience')).toBeInTheDocument(); expect(screen.getByText('Settings')).toBeInTheDocument(); authState.user = { role: 'tenant_admin' }; }); });