Migrate guest v2 achievements and refresh share page
This commit is contained in:
128
resources/js/guest-v2/__tests__/AchievementsScreen.test.tsx
Normal file
128
resources/js/guest-v2/__tests__/AchievementsScreen.test.tsx
Normal file
@@ -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 }) => <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('@tamagui/button', () => ({
|
||||
Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../components/AppShell', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/guest/components/PullToRefresh', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
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(<AchievementsScreen />);
|
||||
|
||||
expect(await screen.findByText('Badges')).toBeInTheDocument();
|
||||
expect(screen.getByText('First upload')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload photo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
121
resources/js/guest-v2/__tests__/GalleryScreen.test.tsx
Normal file
121
resources/js/guest-v2/__tests__/GalleryScreen.test.tsx
Normal file
@@ -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 }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/PhotoFrameTile', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/ShareSheet', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../lib/toast', () => ({
|
||||
pushGuestToast: (...args: unknown[]) => pushGuestToastMock(...args),
|
||||
}));
|
||||
|
||||
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('@tamagui/button', () => ({
|
||||
Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
Camera: () => <span>camera</span>,
|
||||
Sparkles: () => <span>sparkles</span>,
|
||||
Heart: () => <span>heart</span>,
|
||||
Share2: () => <span>share</span>,
|
||||
ChevronLeft: () => <span>left</span>,
|
||||
ChevronRight: () => <span>right</span>,
|
||||
X: () => <span>x</span>,
|
||||
}));
|
||||
|
||||
import GalleryScreen from '../screens/GalleryScreen';
|
||||
|
||||
describe('GalleryScreen', () => {
|
||||
it('clears the photo param and shows a warning when lightbox fails to load', async () => {
|
||||
render(<GalleryScreen />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 }) => (
|
||||
<button type="button" {...rest}>
|
||||
Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
@@ -84,11 +85,17 @@ vi.mock('../services/emotionsApi', () => ({
|
||||
fetchEmotions: vi.fn().mockResolvedValue([{ slug: 'freude', name: 'Joy' }]),
|
||||
}));
|
||||
|
||||
const fetchGalleryMock = vi.fn().mockResolvedValue({ data: [] });
|
||||
|
||||
vi.mock('../services/photosApi', () => ({
|
||||
fetchGallery: vi.fn().mockResolvedValue({ data: [] }),
|
||||
fetchGallery: (...args: unknown[]) => fetchGalleryMock(...args),
|
||||
}));
|
||||
|
||||
const translate = (key: string, fallback?: string) => fallback ?? key;
|
||||
const translate = (key: string, options?: unknown, fallback?: string) => {
|
||||
if (typeof fallback === 'string') return fallback;
|
||||
if (typeof options === 'string') return options;
|
||||
return key;
|
||||
};
|
||||
|
||||
vi.mock('@/guest/i18n/useTranslation', () => ({
|
||||
useTranslation: () => ({ t: translate, locale: 'de' }),
|
||||
@@ -119,7 +126,13 @@ vi.mock('@/guest/hooks/useGuestTaskProgress', () => ({
|
||||
import HomeScreen from '../screens/HomeScreen';
|
||||
|
||||
describe('HomeScreen', () => {
|
||||
beforeEach(() => {
|
||||
navigateMock.mockReset();
|
||||
fetchGalleryMock.mockReset();
|
||||
});
|
||||
|
||||
it('shows task hero content when tasks are enabled', async () => {
|
||||
fetchGalleryMock.mockResolvedValueOnce({ data: [] });
|
||||
useEventDataMock.mockReturnValue({
|
||||
tasksEnabled: true,
|
||||
token: 'demo',
|
||||
@@ -133,6 +146,7 @@ describe('HomeScreen', () => {
|
||||
});
|
||||
|
||||
it('shows capture-ready content when tasks are disabled', () => {
|
||||
fetchGalleryMock.mockResolvedValueOnce({ data: [] });
|
||||
useEventDataMock.mockReturnValue({
|
||||
tasksEnabled: false,
|
||||
token: 'demo',
|
||||
@@ -144,4 +158,22 @@ describe('HomeScreen', () => {
|
||||
expect(screen.getByText('Capture ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload / Take photo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens gallery lightbox from preview tile', async () => {
|
||||
fetchGalleryMock.mockResolvedValueOnce({
|
||||
data: [{ id: 42, thumbnail_url: '/storage/demo.jpg' }],
|
||||
});
|
||||
useEventDataMock.mockReturnValue({
|
||||
tasksEnabled: false,
|
||||
token: 'demo',
|
||||
event: { name: 'Demo Event' },
|
||||
});
|
||||
|
||||
render(<HomeScreen />);
|
||||
|
||||
const previewButton = await screen.findByRole('button', { name: /foto 42/i });
|
||||
fireEvent.click(previewButton);
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith('/e/demo/gallery?photo=42');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@ vi.mock('@tamagui/text', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/button', () => ({
|
||||
Button: ({ children, ...rest }: { children: React.ReactNode }) => (
|
||||
<button type="button" {...rest}>
|
||||
Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
@@ -24,48 +24,53 @@ vi.mock('../components/AppShell', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../context/EventDataContext', () => ({
|
||||
useEventData: () => ({
|
||||
token: 'demo-token',
|
||||
event: { name: 'Demo Event', join_token: 'demo-token' },
|
||||
}),
|
||||
useEventData: () => ({ token: 'demo', event: { name: 'Demo Event' } }),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/usePollStats', () => ({
|
||||
usePollStats: () => ({
|
||||
stats: {
|
||||
onlineGuests: 12,
|
||||
tasksSolved: 0,
|
||||
guestCount: 40,
|
||||
likesCount: 120,
|
||||
latestPhotoAt: null,
|
||||
},
|
||||
}),
|
||||
usePollStats: () => ({ stats: { onlineGuests: 3 } }),
|
||||
}));
|
||||
|
||||
vi.mock('../services/eventLink', () => ({
|
||||
buildEventShareLink: () => 'https://example.test/e/demo',
|
||||
}));
|
||||
|
||||
vi.mock('../services/qrApi', () => ({
|
||||
fetchEventQrCode: () => Promise.resolve({ qr_code_data_url: 'data:image/png;base64,abc' }),
|
||||
fetchEventQrCode: () => Promise.resolve({ qr_code_data_url: '' }),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/guestTheme', () => ({
|
||||
useGuestThemeVariant: () => ({ isDark: false }),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/bento', () => ({
|
||||
getBentoSurfaceTokens: () => ({
|
||||
borderColor: '#eee',
|
||||
borderBottomColor: '#ddd',
|
||||
backgroundColor: '#fff',
|
||||
shadow: 'none',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/toast', () => ({
|
||||
pushGuestToast: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/guest/i18n/useTranslation', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, arg2?: Record<string, string | number> | string, arg3?: string) =>
|
||||
typeof arg2 === 'string' || arg2 === undefined ? (arg2 ?? arg3 ?? key) : (arg3 ?? key),
|
||||
t: (key: string, options?: unknown, fallback?: string) => {
|
||||
if (typeof fallback === 'string') return fallback;
|
||||
if (typeof options === 'string') return options;
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-appearance', () => ({
|
||||
useAppearance: () => ({ resolved: 'light' }),
|
||||
}));
|
||||
|
||||
import ShareScreen from '../screens/ShareScreen';
|
||||
|
||||
describe('ShareScreen', () => {
|
||||
it('shows guests online from stats', async () => {
|
||||
it('renders invite header', async () => {
|
||||
render(<ShareScreen />);
|
||||
|
||||
expect(await screen.findByText('12')).toBeInTheDocument();
|
||||
expect(screen.getByText('Guests joined')).toBeInTheDocument();
|
||||
expect(screen.getByText('Guests online')).toBeInTheDocument();
|
||||
expect(screen.getByAltText('Event QR code')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Invite guests')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user