import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; 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: 'standard', 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' }, }; vi.mock('react-router-dom', () => ({ useNavigate: () => navigateMock, useLocation: () => ({ search: '', pathname: '/event-admin/mobile/dashboard' }), useParams: () => ({ slug: fixtures.event.slug }), })); 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: () => ({ events: [fixtures.event], activeEvent: fixtures.event, hasEvents: true, hasMultipleEvents: false, isLoading: false, 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 }) => , KpiTile: ({ label, value }: { label: string; value: string | number }) => (
{label} {value}
), ActionTile: ({ label }: { label: string }) =>
{label}
, 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/text', () => ({ SizableText: ({ 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', muted: '#64748b', border: '#e2e8f0', surface: '#ffffff', accentSoft: '#eef2ff', primary: '#ff5a5f', surfaceMuted: '#f8fafc', shadow: 'rgba(15,23,42,0.12)', }), })); 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', () => { it('shows package usage progress when a limit is available', () => { render(); expect(screen.getByText('2 of 5 events used')).toBeInTheDocument(); expect(screen.getByText('3 remaining')).toBeInTheDocument(); }); it('hides admin-only shortcuts for members', () => { authState.user = { role: 'member' }; render(); expect(screen.getByText('Moderation & Live Show')).toBeInTheDocument(); expect(screen.queryByText('Event settings')).not.toBeInTheDocument(); expect(screen.queryByText('Live Show settings')).not.toBeInTheDocument(); authState.user = { role: 'tenant_admin' }; }); });