mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
cb7014b378
Costs gets a second view next to the ledger: the expenses as a table grouped by category, with name, date, total, persons and days editable in place and the per person and per day figures worked out beside them. A total with a payer or in a foreign currency stays locked and opens the expense instead, so balances and frozen rates are never changed behind the dialog. In the table view the side column shows a summary by category, day, payer and status. The switch sits in the bar and the choice is remembered; the ledger stays the default. On the Plan tab a place's reservation shows as one Reservation pill whose colour carries the status and which opens the booking, and the drop line of a drag that lost its source row now clears on the next mouse move. The booking detail formats its times in the user's clock, and a hotel without a check-out runs to the end of the last day in the timeline. The wiki pages of the trip are rewritten for the new layout, with new screenshots from the screenshot run (new Plan tab, bookings, transports, dialogs, costs table). The help center guides of the Plan tab are rewritten and translated into every locale, and every new string is translated.
185 lines
8.2 KiB
TypeScript
185 lines
8.2 KiB
TypeScript
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import ConfirmDialog from './ConfirmDialog';
|
|
import Modal from './Modal';
|
|
|
|
describe('ConfirmDialog', () => {
|
|
const onClose = vi.fn();
|
|
const onConfirm = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
onClose.mockClear();
|
|
onConfirm.mockClear();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-001: does not render when isOpen is false', () => {
|
|
render(
|
|
<ConfirmDialog isOpen={false} onClose={onClose} onConfirm={onConfirm} message="Are you sure?" />
|
|
);
|
|
expect(screen.queryByText('Are you sure?')).toBeNull();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-002: renders with default title "Confirm" and message', () => {
|
|
render(
|
|
<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="Are you sure?" />
|
|
);
|
|
expect(screen.getByText('Confirm')).toBeTruthy();
|
|
expect(screen.getByText('Are you sure?')).toBeTruthy();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-003: renders custom title and message', () => {
|
|
render(
|
|
<ConfirmDialog
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
onConfirm={onConfirm}
|
|
title="Remove item"
|
|
message="This cannot be undone."
|
|
/>
|
|
);
|
|
expect(screen.getByText('Remove item')).toBeTruthy();
|
|
expect(screen.getByText('This cannot be undone.')).toBeTruthy();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-004: Cancel button calls onClose', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
|
await user.click(screen.getByRole('button', { name: /cancel/i }));
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
expect(onConfirm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-005: Confirm button calls onConfirm and onClose', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
|
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
expect(onConfirm).toHaveBeenCalledOnce();
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-006: custom button labels render correctly', () => {
|
|
render(
|
|
<ConfirmDialog
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
onConfirm={onConfirm}
|
|
confirmLabel="Yes, remove"
|
|
cancelLabel="Go back"
|
|
/>
|
|
);
|
|
expect(screen.getByRole('button', { name: 'Yes, remove' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: 'Go back' })).toBeTruthy();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-007: Escape key calls onClose', () => {
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-009: a rejecting async onConfirm does not escape as an unhandled rejection', async () => {
|
|
const unhandled = vi.fn();
|
|
process.on('unhandledRejection', unhandled);
|
|
const failing = vi.fn(() => Promise.reject(new Error('delete failed')));
|
|
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={failing} />);
|
|
fireEvent.click(screen.getByRole('button', { name: /delete/i }));
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(failing).toHaveBeenCalledOnce();
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
expect(unhandled).not.toHaveBeenCalled();
|
|
process.off('unhandledRejection', unhandled);
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-010: extra content renders under the message, and the card widens for it', () => {
|
|
render(
|
|
<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="Delete it?">
|
|
<ul aria-label="consequences"><li>Stay at Harbour Hotel</li></ul>
|
|
</ConfirmDialog>
|
|
);
|
|
const list = screen.getByRole('list', { name: 'consequences' });
|
|
expect(list).toHaveTextContent('Stay at Harbour Hotel');
|
|
expect(screen.getByText('Delete it?').compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
expect(document.querySelector('.max-w-md')).not.toBeNull();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-011: Escape takes back the question only, not the dialog it was asked from', () => {
|
|
const closeModal = vi.fn();
|
|
render(
|
|
<>
|
|
<Modal isOpen={true} onClose={closeModal} title="Reorder days">rows</Modal>
|
|
<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="Delete it?" />
|
|
</>
|
|
);
|
|
fireEvent.keyDown(document.body, { key: 'Escape' });
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
expect(closeModal).not.toHaveBeenCalled();
|
|
// Other keys pass through untouched.
|
|
fireEvent.keyDown(document.body, { key: 'Enter' });
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-012: the danger look comes from the danger tokens, the plain one from the accent', () => {
|
|
const { unmount } = render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
|
expect(screen.getByRole('button', { name: /delete/i })).toHaveClass('bg-danger');
|
|
expect(document.querySelector('.bg-danger-soft')).not.toBeNull();
|
|
unmount();
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} danger={false} confirmLabel="Keep" />);
|
|
expect(screen.getByRole('button', { name: 'Keep' })).toHaveClass('bg-accent', 'text-accent-text');
|
|
expect(document.querySelector('.bg-danger-soft')).toBeNull();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-013: a long list scrolls inside the card, and the buttons stay outside it, always in reach', () => {
|
|
render(
|
|
<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="Delete it?">
|
|
<ul aria-label="consequences">{Array.from({ length: 40 }, (_, i) => <li key={i}>Row {i}</li>)}</ul>
|
|
</ConfirmDialog>
|
|
);
|
|
const scroller = screen.getByRole('list', { name: 'consequences' }).closest('.overflow-y-auto') as HTMLElement;
|
|
expect(scroller).not.toBeNull();
|
|
expect(scroller.parentElement).toHaveClass('flex-col');
|
|
expect(scroller.parentElement!.className).toMatch(/max-h-\[/);
|
|
const confirm = screen.getByRole('button', { name: /delete/i });
|
|
expect(scroller.contains(confirm)).toBe(false);
|
|
expect(confirm.parentElement).toHaveClass('flex-none');
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-008: clicking backdrop calls onClose', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="msg" />);
|
|
// The outermost fixed div is the backdrop — click outside the card
|
|
const backdrop = document.querySelector('.fixed') as HTMLElement;
|
|
// fireEvent click on the backdrop element directly
|
|
fireEvent.click(backdrop);
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
describe('on a phone', () => {
|
|
const desktopWidth = window.innerWidth;
|
|
beforeEach(() => { Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 390 }); });
|
|
afterEach(() => { Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: desktopWidth }); });
|
|
|
|
it('FE-COMP-CONFIRM-014: the phone card answers like the desktop one, and a tap on it does not close it', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ConfirmDialog isOpen onClose={onClose} onConfirm={onConfirm} title="Delete note?" message="It is gone for good." confirmLabel="Remove" />);
|
|
await user.click(screen.getByText('It is gone for good.'));
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
await user.click(screen.getByRole('button', { name: 'Remove' }));
|
|
expect(onConfirm).toHaveBeenCalledOnce();
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('FE-COMP-CONFIRM-015: the phone card without danger uses the accent and the default labels', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ConfirmDialog isOpen onClose={onClose} onConfirm={onConfirm} message="Leave?" danger={false}><p>More</p></ConfirmDialog>);
|
|
expect(screen.getByText('Confirm')).toBeInTheDocument();
|
|
expect(screen.getByText('More')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass('bg-accent');
|
|
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
expect(onConfirm).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|