Update admin PWA events, branding, and packages
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { ADMIN_EVENTS_PATH } from '../../constants';
|
||||
|
||||
const fixtures = vi.hoisted(() => ({
|
||||
event: {
|
||||
@@ -41,6 +42,9 @@ const authState = {
|
||||
status: 'authenticated',
|
||||
user: { role: 'tenant_admin' },
|
||||
};
|
||||
const paramsState = {
|
||||
slug: fixtures.event.slug as string | undefined,
|
||||
};
|
||||
const eventContext = {
|
||||
events: [fixtures.event],
|
||||
activeEvent: fixtures.event,
|
||||
@@ -52,7 +56,7 @@ const eventContext = {
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
useLocation: () => ({ search: '', pathname: '/event-admin/mobile/dashboard' }),
|
||||
useParams: () => ({ slug: fixtures.event.slug }),
|
||||
useParams: () => paramsState,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -143,6 +147,13 @@ vi.mock('../components/Sheet', () => ({
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
|
||||
KpiStrip: ({ items }: { items: Array<{ label: string; value: string | number }> }) => (
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<span key={item.label}>{item.label}</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
KpiTile: ({ label, value }: { label: string; value: string | number }) => (
|
||||
<div>
|
||||
<span>{label}</span>
|
||||
@@ -244,6 +255,7 @@ describe('MobileDashboardPage', () => {
|
||||
eventContext.activeEvent = fixtures.event;
|
||||
eventContext.hasEvents = true;
|
||||
eventContext.hasMultipleEvents = false;
|
||||
paramsState.slug = fixtures.event.slug;
|
||||
|
||||
fixtures.activePackage.package_type = 'reseller';
|
||||
fixtures.activePackage.remaining_events = 3;
|
||||
@@ -253,19 +265,27 @@ describe('MobileDashboardPage', () => {
|
||||
window.sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('shows a tasks setup nudge and prompt when no tasks are assigned', () => {
|
||||
it('redirects to the event selector when no event is active', async () => {
|
||||
eventContext.activeEvent = null as unknown as typeof fixtures.event;
|
||||
eventContext.events = [fixtures.event];
|
||||
eventContext.hasEvents = true;
|
||||
paramsState.slug = undefined;
|
||||
|
||||
render(<MobileDashboardPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigateMock).toHaveBeenCalledWith(ADMIN_EVENTS_PATH, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the next-step CTA when tasks are missing', () => {
|
||||
fixtures.event.tasks_count = 0;
|
||||
fixtures.event.engagement_mode = 'tasks';
|
||||
window.sessionStorage.setItem(`tasksDecisionPrompt:${fixtures.event.id}`, 'pending');
|
||||
|
||||
render(<MobileDashboardPage />);
|
||||
|
||||
expect(screen.getByText('Setup needed')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Your event is live with tasks enabled, but no tasks are assigned yet. Choose to add tasks now or disable tasks for this event.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Aufgaben hinzufügen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not redirect endcustomer packages without remaining event quota', () => {
|
||||
@@ -282,21 +302,21 @@ describe('MobileDashboardPage', () => {
|
||||
expect(navigateMock).not.toHaveBeenCalledWith('/event-admin/mobile/billing#packages', { replace: true });
|
||||
});
|
||||
|
||||
it('shows package usage progress when a limit is available', () => {
|
||||
it('shows the activity pulse strip', () => {
|
||||
render(<MobileDashboardPage />);
|
||||
|
||||
expect(screen.getByText('2 of 5 events used')).toBeInTheDocument();
|
||||
expect(screen.getByText('3 remaining')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Photos').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Guests').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Pending').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('hides admin-only shortcuts for members', () => {
|
||||
it('shows shortcut sections for members', () => {
|
||||
authState.user = { role: 'member' };
|
||||
|
||||
render(<MobileDashboardPage />);
|
||||
|
||||
expect(screen.getByText('Moderation & Live Show')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Event settings')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Live Show settings')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Experience')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
|
||||
authState.user = { role: 'tenant_admin' };
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
const backMock = vi.fn();
|
||||
@@ -7,10 +7,11 @@ const navigateMock = vi.fn();
|
||||
const selectEventMock = vi.fn();
|
||||
const refetchMock = vi.fn();
|
||||
const invalidateQueriesMock = vi.fn();
|
||||
const paramsState: { slug?: string } = {};
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
useParams: () => ({}),
|
||||
useParams: () => paramsState,
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
@@ -50,11 +51,18 @@ vi.mock('../components/Primitives', () => ({
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
FloatingActionButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress}>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../components/FormControls', () => ({
|
||||
MobileField: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
MobileDateTimeInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||
MobileDateTimeInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => (
|
||||
<input type="datetime-local" {...props} />
|
||||
),
|
||||
MobileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||
MobileSelect: ({ children, ...props }: { children: React.ReactNode }) => <select {...props}>{children}</select>,
|
||||
MobileTextArea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
|
||||
@@ -108,11 +116,16 @@ vi.mock('../../context/EventContext', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import { getEventTypes } from '../../api';
|
||||
import { getEvent, getEventTypes } from '../../api';
|
||||
import MobileEventFormPage from '../EventFormPage';
|
||||
|
||||
describe('MobileEventFormPage', () => {
|
||||
afterEach(() => {
|
||||
paramsState.slug = undefined;
|
||||
});
|
||||
|
||||
it('renders a save draft button when creating a new event', async () => {
|
||||
paramsState.slug = undefined;
|
||||
await act(async () => {
|
||||
render(<MobileEventFormPage />);
|
||||
});
|
||||
@@ -124,6 +137,7 @@ describe('MobileEventFormPage', () => {
|
||||
});
|
||||
|
||||
it('defaults event type to wedding when available', async () => {
|
||||
paramsState.slug = undefined;
|
||||
vi.mocked(getEventTypes).mockResolvedValueOnce([
|
||||
{ id: 11, slug: 'conference', name: 'Conference', name_translations: {}, icon: null, settings: {} },
|
||||
{ id: 22, slug: 'wedding', name: 'Wedding', name_translations: {}, icon: null, settings: {} },
|
||||
@@ -136,4 +150,24 @@ describe('MobileEventFormPage', () => {
|
||||
const select = screen.getByRole('combobox');
|
||||
expect(select).toHaveValue('22');
|
||||
});
|
||||
|
||||
it('disables the date field when the event is completed', async () => {
|
||||
paramsState.slug = 'past-event';
|
||||
vi.mocked(getEvent).mockResolvedValueOnce({
|
||||
id: 1,
|
||||
name: 'Past event',
|
||||
slug: 'past-event',
|
||||
event_date: '2020-01-01T10:00:00Z',
|
||||
event_type_id: null,
|
||||
event_type: null,
|
||||
status: 'archived',
|
||||
description: null,
|
||||
settings: {},
|
||||
} as any);
|
||||
|
||||
render(<MobileEventFormPage />);
|
||||
|
||||
const dateInput = await screen.findByDisplayValue('2020-01-01T10:00');
|
||||
expect(dateInput).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,10 +15,12 @@ const fixtures = vi.hoisted(() => ({
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
photo_count: 12,
|
||||
like_count: 3,
|
||||
pending_count: 1,
|
||||
guest_count: 22,
|
||||
total: 12,
|
||||
likes: 3,
|
||||
pending_photos: 1,
|
||||
recent_uploads: 4,
|
||||
status: 'published',
|
||||
is_active: true,
|
||||
},
|
||||
invites: [],
|
||||
addons: [],
|
||||
@@ -40,7 +42,12 @@ vi.mock('react-i18next', () => ({
|
||||
}
|
||||
return key;
|
||||
},
|
||||
i18n: { language: 'de' },
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: () => undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useBackNavigation', () => ({
|
||||
@@ -54,6 +61,15 @@ vi.mock('../../api', () => ({
|
||||
getAddonCatalog: vi.fn().mockResolvedValue(fixtures.addons),
|
||||
updateEvent: vi.fn().mockResolvedValue(fixtures.event),
|
||||
createEventAddonCheckout: vi.fn(),
|
||||
getEventEngagement: vi.fn().mockResolvedValue({
|
||||
summary: { totalPhotos: 0, uniqueGuests: 0, tasksSolved: 0, likesTotal: 0 },
|
||||
leaderboards: { uploads: [], likes: [] },
|
||||
highlights: { topPhoto: null, trendingEmotion: null, timeline: [] },
|
||||
feed: [],
|
||||
}),
|
||||
listTenantDataExports: vi.fn().mockResolvedValue([]),
|
||||
requestTenantDataExport: vi.fn(),
|
||||
downloadTenantDataExport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../auth/tokens', () => ({
|
||||
@@ -76,6 +92,14 @@ vi.mock('../components/Primitives', () => ({
|
||||
SkeletonCard: () => <div>Loading...</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/FormControls', () => ({
|
||||
MobileSelect: ({ children, value, onChange }: any) => (
|
||||
<select value={value ?? ''} onChange={onChange}>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../components/LegalConsentSheet', () => ({
|
||||
LegalConsentSheet: () => <div />,
|
||||
}));
|
||||
@@ -125,6 +149,7 @@ vi.mock('../theme', () => ({
|
||||
danger: '#dc2626',
|
||||
surface: '#ffffff',
|
||||
surfaceMuted: '#f9fafb',
|
||||
backdrop: '#111827',
|
||||
accentSoft: '#eef2ff',
|
||||
textStrong: '#0f172a',
|
||||
}),
|
||||
|
||||
@@ -13,14 +13,34 @@ vi.mock('react-router-dom', () => ({
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, fallback?: string | Record<string, unknown>) => {
|
||||
if (typeof fallback === 'string') {
|
||||
return fallback;
|
||||
}
|
||||
if (fallback && typeof fallback === 'object' && typeof fallback.defaultValue === 'string') {
|
||||
return fallback.defaultValue;
|
||||
}
|
||||
return key;
|
||||
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': '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',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -136,7 +156,7 @@ describe('MobileEventsPage', () => {
|
||||
it('renders filters and event list', async () => {
|
||||
render(<MobileEventsPage />);
|
||||
|
||||
expect(await screen.findByText('Filters & Search')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Overview')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Demo Event')).toBeInTheDocument();
|
||||
});
|
||||
@@ -147,7 +167,7 @@ describe('MobileEventsPage', () => {
|
||||
render(<MobileEventsPage />);
|
||||
|
||||
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Create New Event')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('New event')).not.toBeInTheDocument();
|
||||
|
||||
authState.user = { role: 'tenant_admin' };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user