Files
fotospiel-app/resources/js/admin/mobile/__tests__/EventsPage.test.tsx
Codex Agent df00deb0df
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Fix endcustomer package allocation and event create gating
2026-02-06 13:21:11 +01:00

429 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen } from '@testing-library/react';
import * as api from '../../api';
const navigateMock = vi.fn();
const authState = {
user: { role: 'tenant_admin' },
};
vi.mock('react-router-dom', () => ({
useNavigate: () => navigateMock,
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'events.list.title': 'Your events',
'events.list.subtitle': 'Plan memorable moments. Manage everything around your events here.',
'events.list.overview.title': 'Overview',
'events.list.overview.empty': 'No events yet create your first one to get started.',
'events.list.filters.all': 'All',
'events.list.filters.upcoming': 'Upcoming',
'events.list.filters.draft': 'Draft',
'events.list.filters.past': 'Past',
'events.list.actions.create': 'New event',
'events.list.actions.open': 'Open event',
'events.list.empty.filtered': 'No events match this filter.',
'events.list.empty.filteredHint': 'Try a different status or clear your search.',
'events.list.stats.photos': 'Photos',
'events.list.stats.guests': 'Guests',
'events.list.stats.tasks': 'Photo tasks',
'events.workspace.fields.status': 'Status',
'events.detail.pickEvent': 'Select event',
'events.detail.dateTbd': 'Date tbd',
'events.detail.locationPlaceholder': 'Location',
'events.placeholders.untitled': 'Untitled event',
'events.errors.loadFailed': 'Event konnte nicht geladen werden.',
};
return translations[key] ?? key;
},
i18n: {
language: 'en',
},
}),
}));
vi.mock('../../api', () => ({
getEvents: vi.fn().mockResolvedValue([
{
id: 1,
name: 'Demo Event',
slug: 'demo-event',
event_date: '2026-02-19',
settings: {},
},
{
id: 2,
name: { en: 'Summer Gala' },
slug: 'summer-gala',
event_date: '2026-06-01',
settings: {},
},
]),
getTenantPackagesOverview: vi.fn().mockResolvedValue({
packages: [
{
id: 1,
package_id: 1,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
],
activePackage: {
id: 1,
package_id: 1,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
}),
}));
vi.mock('../../auth/tokens', () => ({
isAuthError: () => false,
}));
vi.mock('../../auth/context', () => ({
useAuth: () => authState,
}));
vi.mock('../../lib/apiError', () => ({
getApiErrorMessage: () => 'error',
}));
vi.mock('../../lib/eventFilters', () => ({
buildEventStatusCounts: () => ({ all: 1, upcoming: 1, draft: 0, past: 0 }),
filterEventsByStatus: (events: any[]) => events,
resolveEventStatusKey: () => 'upcoming',
}));
vi.mock('../../lib/eventListStats', () => ({
buildEventListStats: () => ({ photos: 2, guests: 3, tasks: 4 }),
}));
vi.mock('../hooks/useBackNavigation', () => ({
useBackNavigation: () => undefined,
}));
vi.mock('../components/MobileShell', () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Primitives', () => ({
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
FloatingActionButton: ({ label }: { label: string }) => <div>{label}</div>,
SkeletonCard: () => <div>Loading...</div>,
}));
vi.mock('../components/FormControls', () => ({
MobileInput: ({ compact: _compact, ...props }: { compact?: boolean } & React.InputHTMLAttributes<HTMLInputElement>) => (
<input {...props} />
),
}));
vi.mock('@tamagui/card', () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/scroll-view', () => ({
ScrollView: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{children}
</button>
),
}));
vi.mock('@tamagui/toggle-group', () => ({
ToggleGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/separator', () => ({
Separator: () => <div />,
}));
vi.mock('@tamagui/list-item', () => ({
ListItem: ({
title,
iconAfter,
onPress,
}: {
title?: React.ReactNode;
iconAfter?: React.ReactNode;
onPress?: () => void;
}) => (
<div onClick={onPress}>
{title}
{iconAfter}
</div>
),
}));
vi.mock('@tamagui/group', () => ({
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
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('../theme', () => ({
useAdminTheme: () => ({
text: '#111827',
muted: '#6b7280',
subtle: '#94a3b8',
border: '#e5e7eb',
primary: '#ff5a5f',
danger: '#dc2626',
surface: '#ffffff',
surfaceMuted: '#f9fafb',
accentSoft: '#eef2ff',
accent: '#6366f1',
shadow: 'rgba(15,23,42,0.12)',
}),
withAlpha: (value: string) => value,
}));
import MobileEventsPage from '../EventsPage';
describe('MobileEventsPage', () => {
it('renders filters and event list', async () => {
render(<MobileEventsPage />);
expect(await screen.findByText('Overview')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Demo Event')).toBeInTheDocument();
});
it('hides create actions for members', async () => {
authState.user = { role: 'member' };
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
authState.user = { role: 'tenant_admin' };
});
it('filters events by search query', async () => {
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.getByText('Summer Gala')).toBeInTheDocument();
await act(async () => {
fireEvent.change(screen.getByPlaceholderText('Select event'), { target: { value: 'Summer' } });
});
expect(screen.getByText('Summer Gala')).toBeInTheDocument();
expect(screen.queryByText('Demo Event')).not.toBeInTheDocument();
});
it('shows the create button when an active package has remaining events', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 1,
remaining_events: 2,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
],
activePackage: {
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 1,
remaining_events: 2,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('New event')).toBeInTheDocument();
});
it('shows the create button for available endcustomer packages', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 3,
package_id: 3,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
],
activePackage: {
id: 3,
package_id: 3,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('New event')).toBeInTheDocument();
});
it('hides the create button when no remaining events are available', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 5,
remaining_events: 0,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
],
activePackage: {
id: 2,
package_id: 2,
package_name: 'Reseller',
package_type: 'reseller',
included_package_slug: 'standard',
active: true,
used_events: 5,
remaining_events: 0,
price: 240,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: { max_events_per_year: 5 },
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
});
it('hides the create button for consumed endcustomer packages', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 4,
package_id: 4,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 1,
},
],
activePackage: {
id: 4,
package_id: 4,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 1,
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
});
});