import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; const setSearchParamsMock = vi.fn(); const pushGuestToastMock = vi.fn(); const mockEventData = { token: 'demo', event: { name: 'Demo Event', capabilities: { ai_styling: false } }, }; vi.mock('react-router-dom', () => ({ useNavigate: () => vi.fn(), useSearchParams: () => [new URLSearchParams('photo=123'), setSearchParamsMock], })); vi.mock('../context/EventDataContext', () => ({ useEventData: () => mockEventData, })); vi.mock('../hooks/usePollGalleryDelta', () => ({ usePollGalleryDelta: () => ({ data: { photos: [] } }), })); vi.mock('../hooks/usePollStats', () => ({ usePollStats: () => ({ stats: { onlineGuests: 0, guestCount: 0, likesCount: 0 } }), })); vi.mock('@/shared/guest/i18n/useTranslation', () => ({ useTranslation: () => ({ t: (key: string, options?: unknown, fallback?: string) => { if (typeof fallback === 'string') return fallback; if (typeof options === 'string') return options; return key; }, }), })); vi.mock('@/shared/guest/i18n/LocaleContext', () => ({ useLocale: () => ({ locale: 'de' }), })); vi.mock('../lib/guestTheme', () => ({ useGuestThemeVariant: () => ({ isDark: false }), })); vi.mock('../lib/bento', () => ({ getBentoSurfaceTokens: () => ({ backgroundColor: '#fff', borderColor: '#eee', borderBottomColor: '#ddd', shadow: 'none', }), })); const fetchGalleryMock = vi.fn().mockResolvedValue({ data: [] }); const fetchPhotoMock = vi.fn().mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); vi.mock('../services/photosApi', () => ({ fetchGallery: (...args: unknown[]) => fetchGalleryMock(...args), fetchPhoto: (...args: unknown[]) => fetchPhotoMock(...args), likePhoto: vi.fn(), unlikePhoto: vi.fn(), createPhotoShareLink: vi.fn(), deletePhoto: vi.fn(), })); vi.mock('../components/AppShell', () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('../components/PhotoFrameTile', () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, })); vi.mock('../components/ShareSheet', () => ({ default: () => null, })); vi.mock('../components/AiMagicEditSheet', () => ({ default: () => null, })); vi.mock('../lib/toast', () => ({ pushGuestToast: (...args: unknown[]) => pushGuestToastMock(...args), })); 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/button', () => ({ Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => ( ), })); vi.mock('lucide-react', () => ({ Camera: () => camera, Sparkles: () => sparkles, Heart: () => heart, Share2: () => share, ChevronLeft: () => left, ChevronRight: () => right, Loader2: () => loader, Download: () => download, X: () => x, Trash2: () => trash, })); import GalleryScreen from '../screens/GalleryScreen'; describe('GalleryScreen', () => { beforeEach(() => { setSearchParamsMock.mockClear(); pushGuestToastMock.mockClear(); fetchGalleryMock.mockReset(); fetchPhotoMock.mockReset(); mockEventData.token = 'demo'; mockEventData.event = { name: 'Demo Event', capabilities: { ai_styling: false } }; }); afterEach(() => { vi.useRealTimers(); }); it('clears the photo param and shows a warning when lightbox fails to load', async () => { vi.useFakeTimers(); fetchGalleryMock.mockResolvedValue({ data: [] }); fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); render(); for (let i = 0; i < 7; i += 1) { await vi.advanceTimersByTimeAsync(1500); await Promise.resolve(); } await vi.advanceTimersByTimeAsync(1000); await vi.runAllTimersAsync(); await Promise.resolve(); expect(pushGuestToastMock).toHaveBeenCalled(); expect(setSearchParamsMock).toHaveBeenCalled(); const [params] = setSearchParamsMock.mock.calls.at(-1) ?? []; const search = params instanceof URLSearchParams ? params : new URLSearchParams(params); expect(search.get('photo')).toBeNull(); }); it('keeps lightbox open when a seeded photo exists but fetch fails', async () => { vi.useFakeTimers(); fetchGalleryMock.mockResolvedValue({ data: [{ id: 123, thumbnail_url: '/storage/demo.jpg', likes_count: 2 }], }); fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); render(); await Promise.resolve(); await vi.runAllTimersAsync(); expect(fetchGalleryMock).toHaveBeenCalled(); expect(fetchPhotoMock).toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(1000); expect(setSearchParamsMock).not.toHaveBeenCalled(); expect(pushGuestToastMock).not.toHaveBeenCalled(); }); it('does not show ai magic edit action when ai styling is not entitled', async () => { fetchGalleryMock.mockResolvedValue({ data: [{ id: 123, thumbnail_url: '/storage/demo.jpg', likes_count: 2 }], }); fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); render(); await waitFor(() => expect(fetchGalleryMock).toHaveBeenCalled()); await waitFor(() => expect(screen.queryByLabelText('AI Magic Edit')).not.toBeInTheDocument() ); }); it('keeps ai magic edit action hidden while rollout flag is disabled', async () => { mockEventData.event = { name: 'Demo Event', capabilities: { ai_styling: true } }; fetchGalleryMock.mockResolvedValue({ data: [{ id: 123, thumbnail_url: '/storage/demo.jpg', likes_count: 2 }], }); fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); render(); await waitFor(() => expect(fetchGalleryMock).toHaveBeenCalled()); await waitFor(() => expect(screen.queryByLabelText('AI Magic Edit')).not.toBeInTheDocument() ); }); it('uses download_url when downloading from lightbox', async () => { fetchGalleryMock.mockResolvedValue({ data: [{ id: 123, thumbnail_url: '/storage/demo-thumb.jpg', download_url: '/api/v1/gallery/demo/photos/123/download?signature=abc', likes_count: 2, }], }); fetchPhotoMock.mockRejectedValue(Object.assign(new Error('not found'), { status: 404 })); const originalCreateElement = document.createElement.bind(document); const clickSpy = vi.fn(); let createdLink: HTMLAnchorElement | null = null; const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => { const element = originalCreateElement(tagName) as HTMLElement; if (tagName.toLowerCase() === 'a') { createdLink = element as HTMLAnchorElement; (createdLink as any).click = clickSpy; } return element; }); render(); await waitFor(() => expect(fetchGalleryMock).toHaveBeenCalled()); const downloadButton = await screen.findByRole('button', { name: /download/i }); fireEvent.click(downloadButton); expect(clickSpy).toHaveBeenCalled(); expect(createdLink?.getAttribute('href')).toBe('/api/v1/gallery/demo/photos/123/download?signature=abc'); createElementSpy.mockRestore(); }); });