mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
0dd07befa7
The Plan tab was the last trip tab still in the old design. It now reads like Bookings, Costs, Lists, Files and Collab, without losing a function: - Day plan: each day is a card with a head band (day tile with weather, stay and rental pills, a "+" menu for place, stay, transport, transit and note), rows with "..." menus, white badges and a white route bar. - Places column: add, search and filters in three rows, a show dropdown, category and rating filters, and a selection bar at the foot. - Place inspector, day details and the stay editor on the shared dialog frame; the place form in the editor look with the name in the head band. - Clicking a booking or transport anywhere on the plan opens the booking detail popup first; Edit leads to the editor. The popup is one host shared with the Bookings and Transports tabs. - Files and Collab dialogs, notes, polls, links and chat moved to the same frame; shared DialogShell and dialogParts hold the building blocks. - The map hover card shows the place photo and follows the theme. Every new and reworded string is translated into all locales, and a few older English leftovers (help center, cost split, import hints) with them. Phone screens keep their own look.
215 lines
8.9 KiB
TypeScript
215 lines
8.9 KiB
TypeScript
// FE-PLANNER-SELBAR-001 to FE-PLANNER-SELBAR-015
|
|
import userEvent from '@testing-library/user-event';
|
|
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
|
import { buildPlace } from '../../../tests/helpers/factories';
|
|
import { useTranslation } from '../../i18n';
|
|
import type { Place } from '../../types';
|
|
import { PlacesSelectionBar } from './PlacesSidebarSelectionBar';
|
|
import type { SidebarState } from './usePlacesSidebar';
|
|
|
|
const PLACES = [buildPlace({ id: 1 }), buildPlace({ id: 2 }), buildPlace({ id: 3 })];
|
|
|
|
interface BarProps {
|
|
selectedIds?: Set<number>;
|
|
filtered?: Place[];
|
|
isMobile?: boolean;
|
|
collectionsEnabled?: boolean;
|
|
setSelectedIds?: (ids: Set<number>) => void;
|
|
setPendingDeleteIds?: (ids: number[] | null) => void;
|
|
setCategoryPickerOpen?: (open: boolean) => void;
|
|
setSaveToListOpen?: (open: boolean) => void;
|
|
onBulkDeletePlaces?: (ids: number[]) => void;
|
|
markSelectionVisited?: () => Promise<void>;
|
|
markVisitedBusy?: boolean;
|
|
}
|
|
|
|
// The bar reads a handful of fields off the sidebar state; the rest of the hook
|
|
// is irrelevant here, so only those are supplied.
|
|
function Bar(overrides: BarProps) {
|
|
const { t } = useTranslation();
|
|
const state = {
|
|
t,
|
|
selectedIds: new Set<number>(),
|
|
filtered: PLACES,
|
|
isMobile: false,
|
|
collectionsEnabled: false,
|
|
setSelectedIds: () => {},
|
|
setPendingDeleteIds: () => {},
|
|
setCategoryPickerOpen: () => {},
|
|
setSaveToListOpen: () => {},
|
|
onBulkDeletePlaces: undefined,
|
|
markSelectionVisited: async () => {},
|
|
markVisitedBusy: false,
|
|
...overrides,
|
|
};
|
|
return <PlacesSelectionBar {...(state as unknown as SidebarState)} />;
|
|
}
|
|
|
|
describe('PlacesSelectionBar', () => {
|
|
it('FE-PLANNER-SELBAR-001: reports how many places are selected', () => {
|
|
render(<Bar selectedIds={new Set([1, 2])} />);
|
|
expect(screen.getByRole('status', { name: '2 selected' })).toHaveTextContent('2');
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-002: offers "Select all" while the selection is partial', () => {
|
|
render(<Bar selectedIds={new Set([1])} />);
|
|
expect(screen.getByRole('button', { name: 'Select all' })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: 'Deselect all' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-003: "Select all" selects every visible place', async () => {
|
|
const user = userEvent.setup();
|
|
const setSelectedIds = vi.fn((_ids: Set<number>) => {});
|
|
render(<Bar selectedIds={new Set([1])} setSelectedIds={setSelectedIds} />);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Select all' }));
|
|
|
|
expect(setSelectedIds).toHaveBeenCalledWith(new Set([1, 2, 3]));
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-004: a full selection flips the button to "Deselect all" and clears it', async () => {
|
|
const user = userEvent.setup();
|
|
const setSelectedIds = vi.fn((_ids: Set<number>) => {});
|
|
render(<Bar selectedIds={new Set([1, 2, 3])} setSelectedIds={setSelectedIds} />);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Deselect all' }));
|
|
|
|
expect(setSelectedIds).toHaveBeenCalledWith(new Set());
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-005: an empty list keeps the "Select all" wording', async () => {
|
|
const user = userEvent.setup();
|
|
const setSelectedIds = vi.fn((_ids: Set<number>) => {});
|
|
render(<Bar filtered={[]} selectedIds={new Set()} setSelectedIds={setSelectedIds} />);
|
|
|
|
// 0 === 0 would otherwise read as "everything is selected".
|
|
const btn = screen.getByRole('button', { name: 'Select all' });
|
|
await user.click(btn);
|
|
|
|
expect(setSelectedIds).toHaveBeenCalledWith(new Set());
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-006: the category button is disabled without a selection', () => {
|
|
const setCategoryPickerOpen = vi.fn((_open: boolean) => {});
|
|
render(<Bar selectedIds={new Set()} setCategoryPickerOpen={setCategoryPickerOpen} />);
|
|
|
|
const btn = screen.getByRole('button', { name: 'Change category' });
|
|
expect(btn).toBeDisabled();
|
|
fireEvent.click(btn);
|
|
expect(setCategoryPickerOpen).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-007: the category button opens the bulk picker', async () => {
|
|
const user = userEvent.setup();
|
|
const setCategoryPickerOpen = vi.fn((_open: boolean) => {});
|
|
render(<Bar selectedIds={new Set([1])} setCategoryPickerOpen={setCategoryPickerOpen} />);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Change category' }));
|
|
|
|
expect(setCategoryPickerOpen).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-008: the collection button is hidden while the addon is off', () => {
|
|
render(<Bar selectedIds={new Set([1])} collectionsEnabled={false} />);
|
|
expect(screen.queryByRole('button', { name: 'Save to Collection' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-009: the collection button opens the save sheet', async () => {
|
|
const user = userEvent.setup();
|
|
const setSaveToListOpen = vi.fn((_open: boolean) => {});
|
|
render(<Bar selectedIds={new Set([1])} collectionsEnabled setSaveToListOpen={setSaveToListOpen} />);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Save to Collection' }));
|
|
|
|
expect(setSaveToListOpen).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-010: the collection button is disabled without a selection', () => {
|
|
const setSaveToListOpen = vi.fn((_open: boolean) => {});
|
|
render(<Bar selectedIds={new Set()} collectionsEnabled setSaveToListOpen={setSaveToListOpen} />);
|
|
|
|
const btn = screen.getByRole('button', { name: 'Save to Collection' });
|
|
expect(btn).toBeDisabled();
|
|
fireEvent.click(btn);
|
|
expect(setSaveToListOpen).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-011: on desktop delete goes straight to the bulk handler', async () => {
|
|
const user = userEvent.setup();
|
|
const onBulkDeletePlaces = vi.fn((_ids: number[]) => {});
|
|
const setPendingDeleteIds = vi.fn((_ids: number[] | null) => {});
|
|
render(
|
|
<Bar
|
|
selectedIds={new Set([2, 3])}
|
|
onBulkDeletePlaces={onBulkDeletePlaces}
|
|
setPendingDeleteIds={setPendingDeleteIds}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Delete selected' }));
|
|
|
|
expect(onBulkDeletePlaces).toHaveBeenCalledWith([2, 3]);
|
|
expect(setPendingDeleteIds).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-012: on mobile delete arms the confirm dialog instead', async () => {
|
|
const user = userEvent.setup();
|
|
const onBulkDeletePlaces = vi.fn((_ids: number[]) => {});
|
|
const setPendingDeleteIds = vi.fn((_ids: number[] | null) => {});
|
|
render(
|
|
<Bar
|
|
isMobile
|
|
selectedIds={new Set([2, 3])}
|
|
onBulkDeletePlaces={onBulkDeletePlaces}
|
|
setPendingDeleteIds={setPendingDeleteIds}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Delete selected' }));
|
|
|
|
expect(setPendingDeleteIds).toHaveBeenCalledWith([2, 3]);
|
|
expect(onBulkDeletePlaces).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-013: hovering an action shows its name as a tooltip, and leaving hides it', async () => {
|
|
const user = userEvent.setup();
|
|
render(<Bar selectedIds={new Set([1])} collectionsEnabled />);
|
|
|
|
for (const name of ['Select all', 'Change category', 'Save to Collection', 'Mark visited in your lists', 'Delete selected']) {
|
|
const btn = screen.getByRole('button', { name });
|
|
expect(btn).toBeEnabled();
|
|
await user.hover(btn);
|
|
expect(await screen.findByRole('tooltip')).toHaveTextContent(name);
|
|
await user.unhover(btn);
|
|
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
}
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-014: without a selection every action but "Select all" is inert', () => {
|
|
const onBulkDeletePlaces = vi.fn((_ids: number[]) => {});
|
|
const markSelectionVisited = vi.fn(async () => {});
|
|
render(<Bar selectedIds={new Set()} collectionsEnabled onBulkDeletePlaces={onBulkDeletePlaces} markSelectionVisited={markSelectionVisited} />);
|
|
|
|
expect(screen.getByRole('button', { name: 'Select all' })).toBeEnabled();
|
|
for (const name of ['Change category', 'Save to Collection', 'Mark visited in your lists', 'Delete selected']) {
|
|
const btn = screen.getByRole('button', { name });
|
|
expect(btn).toBeDisabled();
|
|
fireEvent.click(btn);
|
|
}
|
|
expect(onBulkDeletePlaces).not.toHaveBeenCalled();
|
|
expect(markSelectionVisited).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PLANNER-SELBAR-015: "mark visited" runs for the selection and is locked while it runs', async () => {
|
|
const user = userEvent.setup();
|
|
const markSelectionVisited = vi.fn(async () => {});
|
|
const { rerender } = render(<Bar selectedIds={new Set([1, 2])} collectionsEnabled markSelectionVisited={markSelectionVisited} />);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Mark visited in your lists' }));
|
|
expect(markSelectionVisited).toHaveBeenCalledTimes(1);
|
|
|
|
rerender(<Bar selectedIds={new Set([1, 2])} collectionsEnabled markSelectionVisited={markSelectionVisited} markVisitedBusy />);
|
|
expect(screen.getByRole('button', { name: 'Mark visited in your lists' })).toBeDisabled();
|
|
});
|
|
});
|