diff --git a/public/lang/de/guest.json b/public/lang/de/guest.json
index 0967ef42..6d6de27e 100644
--- a/public/lang/de/guest.json
+++ b/public/lang/de/guest.json
@@ -1 +1,13 @@
-{}
+{
+ "achievements": {
+ "summary": {
+ "uniqueGuests": "Gäste beteiligt"
+ }
+ },
+ "share": {
+ "invite": {
+ "qrLoading": "QR wird erstellt...",
+ "qrRetry": "Erneut versuchen"
+ }
+ }
+}
diff --git a/public/lang/en/guest.json b/public/lang/en/guest.json
index 0967ef42..486d317c 100644
--- a/public/lang/en/guest.json
+++ b/public/lang/en/guest.json
@@ -1 +1,13 @@
-{}
+{
+ "achievements": {
+ "summary": {
+ "uniqueGuests": "Guests involved"
+ }
+ },
+ "share": {
+ "invite": {
+ "qrLoading": "Generating QR...",
+ "qrRetry": "Retry"
+ }
+ }
+}
diff --git a/resources/js/guest-v2/__tests__/AchievementsScreen.test.tsx b/resources/js/guest-v2/__tests__/AchievementsScreen.test.tsx
new file mode 100644
index 00000000..f0f4a8ea
--- /dev/null
+++ b/resources/js/guest-v2/__tests__/AchievementsScreen.test.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+
+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('../components/AppShell', () => ({
+ default: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('@/guest/components/PullToRefresh', () => ({
+ default: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('../context/EventDataContext', () => ({
+ useEventData: () => ({ token: 'demo', tasksEnabled: true }),
+}));
+
+vi.mock('../context/GuestIdentityContext', () => ({
+ useOptionalGuestIdentity: () => ({ name: 'Alex' }),
+}));
+
+vi.mock('@/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('@/guest/i18n/LocaleContext', () => ({
+ useLocale: () => ({ locale: 'de' }),
+}));
+
+vi.mock('../lib/guestTheme', () => ({
+ useGuestThemeVariant: () => ({ isDark: false }),
+}));
+
+vi.mock('../lib/bento', () => ({
+ getBentoSurfaceTokens: () => ({
+ borderColor: '#eee',
+ borderBottomColor: '#ddd',
+ backgroundColor: '#fff',
+ shadow: 'none',
+ }),
+}));
+
+vi.mock('../lib/routes', () => ({
+ buildEventPath: (_token: string, path: string) => `/e/demo${path}`,
+}));
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+}));
+
+vi.mock('@/guest/lib/localizeTaskLabel', () => ({
+ localizeTaskLabel: (value: string | null) => value,
+}));
+
+vi.mock('../services/achievementsApi', () => ({
+ fetchAchievements: vi.fn().mockResolvedValue({
+ summary: { totalPhotos: 12, uniqueGuests: 4, tasksSolved: 2, likesTotal: 30 },
+ personal: {
+ guestName: 'Alex',
+ photos: 3,
+ tasks: 1,
+ likes: 5,
+ badges: [
+ { id: 'b1', title: 'First upload', description: 'Upload one photo', earned: true, progress: 1, target: 1 },
+ ],
+ },
+ leaderboards: {
+ uploads: [{ guest: 'Sam', photos: 6, likes: 10 }],
+ likes: [{ guest: 'Kai', photos: 2, likes: 12 }],
+ },
+ highlights: {
+ topPhoto: {
+ photoId: 1,
+ guest: 'Sam',
+ likes: 10,
+ task: 'Smile',
+ createdAt: new Date().toISOString(),
+ thumbnail: null,
+ },
+ trendingEmotion: { emotionId: 1, name: 'Joy', count: 8 },
+ timeline: [{ date: '2025-01-01', photos: 4, guests: 2 }],
+ },
+ feed: [
+ {
+ photoId: 9,
+ guest: 'Mia',
+ task: 'Dance',
+ likes: 3,
+ createdAt: new Date().toISOString(),
+ thumbnail: null,
+ },
+ ],
+ }),
+}));
+
+import AchievementsScreen from '../screens/AchievementsScreen';
+
+describe('AchievementsScreen', () => {
+ it('renders personal achievements content', async () => {
+ render();
+
+ expect(await screen.findByText('Badges')).toBeInTheDocument();
+ expect(screen.getByText('First upload')).toBeInTheDocument();
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
+ });
+});
diff --git a/resources/js/guest-v2/__tests__/GalleryScreen.test.tsx b/resources/js/guest-v2/__tests__/GalleryScreen.test.tsx
new file mode 100644
index 00000000..a9311cbc
--- /dev/null
+++ b/resources/js/guest-v2/__tests__/GalleryScreen.test.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { render, waitFor } from '@testing-library/react';
+
+const setSearchParamsMock = vi.fn();
+const pushGuestToastMock = vi.fn();
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useSearchParams: () => [new URLSearchParams('photo=123'), setSearchParamsMock],
+}));
+
+vi.mock('../context/EventDataContext', () => ({
+ useEventData: () => ({ token: 'demo', event: { name: 'Demo Event' } }),
+}));
+
+vi.mock('../hooks/usePollGalleryDelta', () => ({
+ usePollGalleryDelta: () => ({ data: { photos: [] } }),
+}));
+
+vi.mock('../hooks/usePollStats', () => ({
+ usePollStats: () => ({ stats: { onlineGuests: 0, guestCount: 0, likesCount: 0 } }),
+}));
+
+vi.mock('@/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('@/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(),
+}));
+
+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('../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,
+ X: () => x,
+}));
+
+import GalleryScreen from '../screens/GalleryScreen';
+
+describe('GalleryScreen', () => {
+ it('clears the photo param and shows a warning when lightbox fails to load', async () => {
+ render();
+
+ await waitFor(() => {
+ 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();
+ });
+});
diff --git a/resources/js/guest-v2/__tests__/HomeScreen.test.tsx b/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
index e8d31d13..0ed5a81e 100644
--- a/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
+++ b/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
@@ -1,10 +1,11 @@
import React from 'react';
-import { describe, expect, it, vi } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen } from '@testing-library/react';
const useEventDataMock = vi.fn();
+const navigateMock = vi.fn();
vi.mock('react-router-dom', () => ({
- useNavigate: () => vi.fn(),
+ useNavigate: () => navigateMock,
}));
vi.mock('../context/EventDataContext', () => ({
@@ -21,8 +22,8 @@ vi.mock('@tamagui/text', () => ({
}));
vi.mock('@tamagui/button', () => ({
- Button: ({ children, ...rest }: { children: React.ReactNode }) => (
-