mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
72a82b3c44
214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out.
541 lines
21 KiB
TypeScript
541 lines
21 KiB
TypeScript
import React from 'react';
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor, within } from '../../../helpers/render';
|
|
import MDashboard from '../../../../src/mobile/screens/dashboard/MDashboard';
|
|
import { tripsApi } from '../../../../src/api/client';
|
|
import { useAuthStore } from '../../../../src/store/authStore';
|
|
import { useInAppNotificationStore } from '../../../../src/store/inAppNotificationStore';
|
|
import { usePluginStore } from '../../../../src/store/pluginStore';
|
|
import type { TripCardBadge } from '../../../../src/api/client';
|
|
import type { DashboardTrip } from '../../../../src/pages/dashboard/dashboardModel';
|
|
|
|
// FE-MOB-DASH-001 onwards
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
dash: {} as Record<string, unknown>,
|
|
badges: {} as Record<number, unknown[]>,
|
|
}));
|
|
|
|
vi.mock('../../../../src/pages/dashboard/useDashboard', () => ({
|
|
useDashboard: () => mocks.dash,
|
|
}));
|
|
|
|
// The badge lookup is a network hook of its own; the screen only cares what it returns.
|
|
vi.mock('../../../../src/components/Plugins/TripCardBadges', () => ({
|
|
useTripCardBadges: () => (tripId: number) => mocks.badges[tripId] ?? [],
|
|
}));
|
|
|
|
const iso = (offsetDays: number): string => {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() + offsetDays);
|
|
return d.toISOString().split('T')[0];
|
|
};
|
|
|
|
function buildTrip(over: Partial<DashboardTrip> = {}): DashboardTrip {
|
|
return {
|
|
id: 1, user_id: 1, title: 'Japan 2026', currency: 'EUR', is_archived: 0,
|
|
reminder_days: 0, start_date: iso(-3), end_date: iso(10),
|
|
day_count: 14, place_count: 32, shared_count: 2,
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function buildDash(over: Record<string, unknown> = {}): Record<string, unknown> {
|
|
return {
|
|
locale: 'en',
|
|
t: (key: string) => key,
|
|
navigate: vi.fn(),
|
|
spotlight: null,
|
|
upcoming: [],
|
|
gridTrips: [],
|
|
isLoading: false,
|
|
loadError: false,
|
|
retryLoad: vi.fn(),
|
|
tripFilter: 'planned',
|
|
setTripFilter: vi.fn(),
|
|
viewMode: 'grid',
|
|
toggleViewMode: vi.fn(),
|
|
showForm: false,
|
|
setShowForm: vi.fn(),
|
|
editingTrip: null,
|
|
setEditingTrip: vi.fn(),
|
|
deleteTrip: null,
|
|
setDeleteTrip: vi.fn(),
|
|
copyTrip: null,
|
|
setCopyTrip: vi.fn(),
|
|
applyCoverUpdate: vi.fn(),
|
|
handleCreate: vi.fn(),
|
|
handleUpdate: vi.fn(),
|
|
confirmDelete: vi.fn(),
|
|
handleArchive: vi.fn(),
|
|
handleUnarchive: vi.fn(),
|
|
confirmCopy: vi.fn(),
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe('MDashboard', () => {
|
|
beforeEach(() => {
|
|
mocks.dash = buildDash();
|
|
mocks.badges = {};
|
|
usePluginStore.setState({ plugins: [], loaded: true });
|
|
useAuthStore.setState({
|
|
isAuthenticated: true,
|
|
user: { id: 1, username: 'Maurice', email: 'maurice@trek.app', role: 'user', avatar_url: '' } as never,
|
|
});
|
|
useInAppNotificationStore.setState({ unreadCount: 0, fetchUnreadCount: async () => {} });
|
|
});
|
|
|
|
it('FE-MOB-DASH-001: renders the ongoing spotlight with badge, progress and grid cards', () => {
|
|
mocks.dash = buildDash({
|
|
spotlight: buildTrip(),
|
|
gridTrips: [buildTrip({ id: 2, title: 'Lisbon', start_date: iso(30), end_date: iso(40) })],
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('Japan 2026')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.status.ongoing')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.mobile.spotlightDayOf')).toBeInTheDocument();
|
|
// Stat pills reuse the desktop hero keys (no mobile-only duplicates).
|
|
expect(screen.getByText('dashboard.hero.destinationMany')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.hero.travelerMany')).toBeInTheDocument();
|
|
expect(screen.getByText('Lisbon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-002: archived filter swaps card actions to restore + permanent delete', () => {
|
|
const handleUnarchive = vi.fn();
|
|
mocks.dash = buildDash({
|
|
tripFilter: 'archive',
|
|
handleUnarchive,
|
|
gridTrips: [buildTrip({ id: 3, title: 'Iceland', is_archived: 1 })],
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.queryByRole('button', { name: 'common.edit' })).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.restore' }));
|
|
expect(handleUnarchive).toHaveBeenCalledWith(3);
|
|
expect(screen.getByRole('button', { name: 'common.delete' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-003: the view toggle persists through the shared hook action', () => {
|
|
const toggleViewMode = vi.fn();
|
|
mocks.dash = buildDash({ toggleViewMode, gridTrips: [buildTrip()] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.aria.toggleView' }));
|
|
expect(toggleViewMode).toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-MOB-DASH-004: an upcoming reservation opens the trip on its bookings tab', () => {
|
|
mocks.dash = buildDash({
|
|
upcoming: [{ id: 9, trip_id: 7, title: 'teamLab Planets', type: 'ticket', reservation_time: null, day_date: iso(2) }],
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByText('teamLab Planets'));
|
|
expect(sessionStorage.getItem('trip-tab-7')).toBe('buchungen');
|
|
});
|
|
|
|
it('FE-MOB-DASH-005: a failed load shows the retry banner', () => {
|
|
const retryLoad = vi.fn();
|
|
mocks.dash = buildDash({ loadError: true, retryLoad });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByRole('alert')).toHaveTextContent('dashboard.loadErrorBanner');
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.retry' }));
|
|
expect(retryLoad).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-MOB-DASH-006: the empty state opens a blank create sheet', () => {
|
|
const setShowForm = vi.fn();
|
|
const setEditingTrip = vi.fn();
|
|
mocks.dash = buildDash({ setShowForm, setEditingTrip });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('dashboard.emptyTitle')).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.emptyButton' }));
|
|
|
|
expect(setEditingTrip).toHaveBeenCalledWith(null);
|
|
expect(setShowForm).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('FE-MOB-DASH-007: a still-loading dashboard shows no empty state', () => {
|
|
mocks.dash = buildDash({ isLoading: true });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.queryByText('dashboard.emptyTitle')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-008: demo mode adds the banner', () => {
|
|
mocks.dash = buildDash({ demoMode: true });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText(/demo/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-009: unread notifications mark the bell and it opens the list', () => {
|
|
const navigate = vi.fn();
|
|
mocks.dash = buildDash({ navigate });
|
|
useInAppNotificationStore.setState({ unreadCount: 3, fetchUnreadCount: async () => {} });
|
|
render(<MDashboard />);
|
|
|
|
const bell = screen.getByRole('button', { name: 'notifications.title' });
|
|
expect(bell.querySelector('span[aria-hidden]')).toBeInTheDocument();
|
|
|
|
fireEvent.click(bell);
|
|
expect(navigate).toHaveBeenCalledWith('/notifications');
|
|
});
|
|
|
|
it('FE-MOB-DASH-010: the avatar button toggles the user menu', () => {
|
|
render(<MDashboard />);
|
|
const avatar = screen.getByRole('button', { name: 'nav.profile' });
|
|
|
|
expect(avatar).toHaveTextContent('M');
|
|
expect(avatar).toHaveAttribute('aria-expanded', 'false');
|
|
|
|
fireEvent.click(avatar);
|
|
expect(avatar).toHaveAttribute('aria-expanded', 'true');
|
|
});
|
|
|
|
it('FE-MOB-DASH-011: a stored avatar replaces the initial', () => {
|
|
useAuthStore.setState({
|
|
isAuthenticated: true,
|
|
user: { id: 1, username: 'Maurice', email: 'm@trek.app', role: 'user', avatar_url: '/uploads/avatars/m.jpg' } as never,
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
const avatar = screen.getByRole('button', { name: 'nav.profile' });
|
|
expect(avatar.querySelector('img')).toHaveAttribute('src', '/uploads/avatars/m.jpg');
|
|
});
|
|
|
|
it('FE-MOB-DASH-012: the calendar action opens the all-trips subscribe dialog', () => {
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.subscribeAllTrips' }));
|
|
|
|
expect(screen.getByText('dashboard.subscribeAllTripsDesc')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-013: the delete sheet names the trip and runs the confirm action', () => {
|
|
const confirmDelete = vi.fn();
|
|
const setDeleteTrip = vi.fn();
|
|
mocks.dash = buildDash({ deleteTrip: buildTrip({ title: 'Iceland' }), confirmDelete, setDeleteTrip });
|
|
render(<MDashboard />);
|
|
|
|
const sheet = screen.getByRole('dialog', { name: 'common.delete' });
|
|
expect(within(sheet).getByText('dashboard.confirm.delete')).toBeInTheDocument();
|
|
|
|
// The sheet's own chrome uses the real translations, the props the mocked t.
|
|
fireEvent.click(within(sheet).getByText('Cancel'));
|
|
expect(setDeleteTrip).toHaveBeenCalledWith(null);
|
|
|
|
fireEvent.click(within(sheet).getAllByText('common.delete')[1]);
|
|
expect(confirmDelete).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-MOB-DASH-014: the copy sheet confirms the duplicate', () => {
|
|
const confirmCopy = vi.fn();
|
|
mocks.dash = buildDash({ copyTrip: buildTrip({ title: 'Iceland' }), confirmCopy });
|
|
render(<MDashboard />);
|
|
|
|
const sheet = screen.getByRole('dialog', { name: 'dashboard.confirm.copy.title' });
|
|
expect(within(sheet).getByText('Iceland')).toBeInTheDocument();
|
|
|
|
fireEvent.click(within(sheet).getByText('dashboard.confirm.copy.confirm'));
|
|
expect(confirmCopy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-MOB-DASH-015: the list layout adds an archive action and the day/place stats', () => {
|
|
const handleArchive = vi.fn();
|
|
mocks.dash = buildDash({
|
|
viewMode: 'list',
|
|
handleArchive,
|
|
gridTrips: [buildTrip({ id: 4, title: 'Lisbon', day_count: 12, place_count: 30, shared_count: 1 })],
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('12')).toBeInTheDocument();
|
|
expect(screen.getByText('30')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.card.buddyOne')).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.archive' }));
|
|
expect(handleArchive).toHaveBeenCalledWith(4);
|
|
});
|
|
|
|
it('FE-MOB-DASH-016: an undated trip renders dashes instead of a range', () => {
|
|
mocks.dash = buildDash({ gridTrips: [buildTrip({ id: 5, start_date: null, end_date: null })] });
|
|
render(<MDashboard />);
|
|
|
|
const card = screen.getByText('Japan 2026').closest('[role="button"]') as HTMLElement;
|
|
expect(within(card).getAllByText('—')).toHaveLength(2);
|
|
expect(within(card).getByText('dashboard.card.idea')).toBeInTheDocument();
|
|
});
|
|
|
|
it.each([
|
|
[{ is_archived: 1 }, 'dashboard.archived'],
|
|
[{ start_date: iso(0), end_date: null }, 'dashboard.status.today'],
|
|
[{ start_date: iso(1), end_date: null }, 'dashboard.status.tomorrow'],
|
|
[{ start_date: iso(10), end_date: null }, 'dashboard.mobile.inDays'],
|
|
[{ start_date: iso(200), end_date: null }, 'dashboard.mobile.inMonths'],
|
|
[{ start_date: iso(-20), end_date: iso(-10) }, 'dashboard.mobile.completed'],
|
|
] as Array<[Partial<DashboardTrip>, string]>)(
|
|
'FE-MOB-DASH-017: the card badge follows the trip state (%#)',
|
|
(over, label) => {
|
|
mocks.dash = buildDash({ gridTrips: [buildTrip({ id: 6, ...over })] });
|
|
render(<MDashboard />);
|
|
|
|
// The filter segments reuse some of these labels, so assert on the card itself.
|
|
const card = screen.getByText('Japan 2026').closest('[role="button"]') as HTMLElement;
|
|
expect(within(card).getByText(label)).toBeInTheDocument();
|
|
},
|
|
);
|
|
|
|
it('FE-MOB-DASH-018: plugin badges render as chips, linked ones as anchors', () => {
|
|
mocks.badges = {
|
|
8: [
|
|
{ pluginId: 'p', id: 'a', label: 'Weather', value: '21°', tone: 'success' },
|
|
{ pluginId: 'p', id: 'b', label: 'Docs', value: '', tone: 'warn', url: 'https://example.com' },
|
|
] as TripCardBadge[],
|
|
};
|
|
mocks.dash = buildDash({ gridTrips: [buildTrip({ id: 8, title: 'Oslo' })] });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('21°')).toBeInTheDocument();
|
|
const link = screen.getByText('Docs').closest('a') as HTMLAnchorElement;
|
|
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
// The chip must not open the card underneath it.
|
|
fireEvent.click(link);
|
|
expect((mocks.dash.navigate as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-MOB-DASH-019: dashboard widget plugins render below the blocks', () => {
|
|
usePluginStore.setState({
|
|
plugins: [
|
|
{ id: 'w1', name: 'Packing helper', type: 'widget', icon: null },
|
|
{ id: 'h1', name: 'Hero widget', type: 'widget', icon: null, slot: 'hero' },
|
|
] as never,
|
|
loaded: true,
|
|
});
|
|
mocks.dash = buildDash({ spotlight: buildTrip() });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('Packing helper')).toBeInTheDocument();
|
|
expect(screen.queryByText('Hero widget')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-020: a card action does not open the trip underneath it', () => {
|
|
const navigate = vi.fn();
|
|
const setCopyTrip = vi.fn();
|
|
mocks.dash = buildDash({ navigate, setCopyTrip, gridTrips: [buildTrip({ id: 9, title: 'Oslo' })] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.aria.duplicate' }));
|
|
|
|
expect(setCopyTrip).toHaveBeenCalled();
|
|
expect(navigate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-MOB-DASH-021: pressing Enter on a card opens the trip', () => {
|
|
const navigate = vi.fn();
|
|
mocks.dash = buildDash({ navigate, gridTrips: [buildTrip({ id: 9, title: 'Oslo' })] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.keyDown(screen.getByText('Oslo').closest('[role="button"]') as HTMLElement, { key: 'Enter' });
|
|
|
|
expect(navigate).toHaveBeenCalledWith('/trips/9');
|
|
});
|
|
|
|
it('FE-MOB-DASH-022: an upcoming spotlight counts down instead of showing progress', () => {
|
|
mocks.dash = buildDash({
|
|
spotlight: buildTrip({ start_date: iso(4), end_date: iso(9), cover_image: '/uploads/covers/x.jpg' }),
|
|
});
|
|
const { container } = render(<MDashboard />);
|
|
|
|
expect(screen.getByText('dashboard.hero.badgeNext')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.mobile.inDays')).toBeInTheDocument();
|
|
expect(container.querySelector('img[src="/uploads/covers/x.jpg"]')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-023: a finished spotlight is labelled as recent', () => {
|
|
mocks.dash = buildDash({ spotlight: buildTrip({ start_date: iso(-20), end_date: iso(-10), day_count: 1 }) });
|
|
render(<MDashboard />);
|
|
|
|
expect(screen.getByText('dashboard.hero.badgeRecent')).toBeInTheDocument();
|
|
expect(screen.getByText('dashboard.mobile.spotlightDayOne')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-024: the spotlight opens on Enter as well as on click', () => {
|
|
const navigate = vi.fn();
|
|
mocks.dash = buildDash({ navigate, spotlight: buildTrip({ id: 12 }) });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.keyDown(screen.getByText('Japan 2026').closest('[role="button"]') as HTMLElement, { key: 'Enter' });
|
|
|
|
expect(navigate).toHaveBeenCalledWith('/trips/12');
|
|
});
|
|
|
|
it('FE-MOB-DASH-025: editing a card hands the trip to the create/edit sheet', () => {
|
|
const setEditingTrip = vi.fn();
|
|
const setShowForm = vi.fn();
|
|
const trip = buildTrip({ id: 13, title: 'Oslo' });
|
|
mocks.dash = buildDash({ setEditingTrip, setShowForm, gridTrips: [trip] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.edit' }));
|
|
|
|
expect(setEditingTrip).toHaveBeenCalledWith(trip);
|
|
expect(setShowForm).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('FE-MOB-DASH-026: the mobile widgets render in the configured block order', () => {
|
|
mocks.dash = buildDash();
|
|
render(<MDashboard />);
|
|
|
|
// The widget panels bring their own translations rather than the mocked hook's t.
|
|
expect(screen.getByText('Currency')).toBeInTheDocument();
|
|
expect(screen.getByText('Timezones')).toBeInTheDocument();
|
|
expect(screen.getByText('Upcoming reservations')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-DASH-027: a malformed date renders a dash instead of crashing', () => {
|
|
mocks.dash = buildDash({ gridTrips: [buildTrip({ id: 14, start_date: 'not-a-date', end_date: iso(3) })] });
|
|
render(<MDashboard />);
|
|
|
|
const card = screen.getByText('Japan 2026').closest('[role="button"]') as HTMLElement;
|
|
expect(within(card).getAllByText('—')).toHaveLength(1);
|
|
});
|
|
|
|
it('FE-MOB-DASH-028: the archived card duplicates and deletes through the hook', () => {
|
|
const setCopyTrip = vi.fn();
|
|
const setDeleteTrip = vi.fn();
|
|
const trip = buildTrip({ id: 15, title: 'Iceland', is_archived: 1 });
|
|
mocks.dash = buildDash({ tripFilter: 'archive', setCopyTrip, setDeleteTrip, gridTrips: [trip] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.aria.duplicate' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.delete' }));
|
|
|
|
expect(setCopyTrip).toHaveBeenCalledWith(trip);
|
|
expect(setDeleteTrip).toHaveBeenCalledWith(trip);
|
|
});
|
|
|
|
it('FE-MOB-DASH-029: a grid card deletes through the hook', () => {
|
|
const setDeleteTrip = vi.fn();
|
|
const trip = buildTrip({ id: 16, title: 'Oslo' });
|
|
mocks.dash = buildDash({ setDeleteTrip, gridTrips: [trip] });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.delete' }));
|
|
|
|
expect(setDeleteTrip).toHaveBeenCalledWith(trip);
|
|
});
|
|
|
|
it('FE-MOB-DASH-030: a list card opens its trip by click and by Enter', () => {
|
|
const navigate = vi.fn();
|
|
mocks.dash = buildDash({ viewMode: 'list', navigate, gridTrips: [buildTrip({ id: 17, title: 'Oslo' })] });
|
|
render(<MDashboard />);
|
|
const card = screen.getByText('Oslo').closest('[role="button"]') as HTMLElement;
|
|
|
|
fireEvent.click(card);
|
|
fireEvent.keyDown(card, { key: 'Enter' });
|
|
|
|
expect(navigate).toHaveBeenNthCalledWith(1, '/trips/17');
|
|
expect(navigate).toHaveBeenNthCalledWith(2, '/trips/17');
|
|
});
|
|
|
|
it('FE-MOB-DASH-031: the brand tile scrolls the shell content back to the top', () => {
|
|
// The screen scrolls inside the shell's container, not the window.
|
|
const scroller = document.createElement('div');
|
|
scroller.className = 'overflow-y-auto';
|
|
const scrollTo = vi.fn();
|
|
scroller.scrollTo = scrollTo;
|
|
document.body.appendChild(scroller);
|
|
render(<MDashboard />, { container: scroller });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'TREK' }));
|
|
|
|
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
|
scroller.remove();
|
|
});
|
|
|
|
it('FE-MOB-DASH-032: the user menu backdrop closes it again', () => {
|
|
const { container } = render(<MDashboard />);
|
|
const avatar = screen.getByRole('button', { name: 'nav.profile' });
|
|
|
|
fireEvent.click(avatar);
|
|
fireEvent.click(container.querySelector('.fixed.inset-0') as HTMLElement);
|
|
|
|
expect(avatar).toHaveAttribute('aria-expanded', 'false');
|
|
});
|
|
|
|
it('FE-MOB-DASH-033: closing the trip sheet resets the form state', () => {
|
|
const setShowForm = vi.fn();
|
|
const setEditingTrip = vi.fn();
|
|
mocks.dash = buildDash({ showForm: true, setShowForm, setEditingTrip });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(within(screen.getByRole('dialog')).getByText('Cancel'));
|
|
|
|
expect(setShowForm).toHaveBeenCalledWith(false);
|
|
expect(setEditingTrip).toHaveBeenCalledWith(null);
|
|
});
|
|
|
|
it('FE-MOB-DASH-034: archiving from the edit sheet runs the matching hook action', () => {
|
|
const handleArchive = vi.fn();
|
|
const handleUnarchive = vi.fn();
|
|
mocks.dash = buildDash({
|
|
showForm: true, editingTrip: buildTrip({ id: 18 }), handleArchive, handleUnarchive,
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByText('Archive'));
|
|
expect(handleArchive).toHaveBeenCalledWith(18);
|
|
|
|
mocks.dash = buildDash({
|
|
showForm: true, editingTrip: buildTrip({ id: 18, is_archived: 1 }), handleArchive, handleUnarchive,
|
|
});
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByText('Restore'));
|
|
expect(handleUnarchive).toHaveBeenCalledWith(18);
|
|
});
|
|
|
|
it('FE-MOB-DASH-035: a cover uploaded in the sheet is patched into the trip list', async () => {
|
|
const applyCoverUpdate = vi.fn();
|
|
vi.spyOn(tripsApi, 'uploadCover').mockResolvedValue({ cover_image: '/uploads/covers/n.jpg' });
|
|
mocks.dash = buildDash({ showForm: true, editingTrip: buildTrip({ id: 19 }), applyCoverUpdate });
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.change(document.querySelector('input[type="file"]') as HTMLInputElement, {
|
|
target: { files: [new File(['x'], 'c.png', { type: 'image/png' })] },
|
|
});
|
|
|
|
await waitFor(() => expect(applyCoverUpdate).toHaveBeenCalledWith(19, '/uploads/covers/n.jpg'));
|
|
});
|
|
|
|
it('FE-MOB-DASH-036: dismissing the copy sheet clears the pending trip', () => {
|
|
const setCopyTrip = vi.fn();
|
|
mocks.dash = buildDash({ copyTrip: buildTrip({ title: 'Iceland' }), setCopyTrip });
|
|
render(<MDashboard />);
|
|
|
|
const sheet = screen.getByRole('dialog', { name: 'dashboard.confirm.copy.title' });
|
|
fireEvent.click(within(sheet).getByText('Cancel'));
|
|
|
|
expect(setCopyTrip).toHaveBeenCalledWith(null);
|
|
});
|
|
|
|
it('FE-MOB-DASH-037: the subscribe dialog can be closed again', async () => {
|
|
render(<MDashboard />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'dashboard.subscribeAllTrips' }));
|
|
expect(screen.getByText('dashboard.subscribeAllTripsDesc')).toBeInTheDocument();
|
|
|
|
// The dialog's close control is icon-only; it is the first button of the card.
|
|
const card = screen.getByText('dashboard.subscribeAllTripsDesc').parentElement as HTMLElement;
|
|
fireEvent.click(card.querySelector('button') as HTMLElement);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.queryByText('dashboard.subscribeAllTripsDesc')).not.toBeInTheDocument());
|
|
});
|
|
});
|