mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
feat(ui): Costs bar like the other tabs, desktop packing list in the phone's look
The Costs tab opens with the same bar as Transports, Bookings, Lists and Files: the tab's name, the trip's dates and days in one pill, the travelers, and Settle up and Add expense on the right. The desktop packing list takes the phone's look without losing anything it shows. Category cards get a tinted head band, rows get hairlines and filled checkboxes, owner, quantity and weight read as small badges, and the bin and a menu with Move to List, Sharing, Rename and Delete replace the row of icons. What a row does not use stays dimmed until hover or focus, so nothing leaves a gap. A click on the name renames, ticked items too. Add item opens a composer shaped like the next row, Add list moved into the Lists bar, Add list and Save as template ask for the name in a dialog, and Remove checked sits beside the progress bar. The Bags panel becomes a card, and the item menu, the bag picker, the list menu and the bag member picker share one popover look and close on a click beside them. Help center pictures for Costs and Lists are re-rendered, the Lists guide texts follow the new controls in all 27 languages, and the wiki pages are updated. The phone screens are unchanged.
This commit is contained in:
@@ -248,8 +248,8 @@ describe('PackingListPanel', () => {
|
||||
);
|
||||
render(<PackingListPanel tripId={1} items={[item]} />);
|
||||
|
||||
// Click the rename (pencil) button
|
||||
await user.click(screen.getByTitle('Rename'));
|
||||
// A click on the name opens it for renaming
|
||||
await user.click(screen.getByText('Sunscreen'));
|
||||
|
||||
// Input appears pre-filled with 'Sunscreen'
|
||||
const input = screen.getByDisplayValue('Sunscreen');
|
||||
@@ -275,8 +275,8 @@ describe('PackingListPanel', () => {
|
||||
);
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[item]} />);
|
||||
|
||||
// The toggle button contains the Square icon for unchecked items
|
||||
const toggleBtn = container.querySelector('svg.lucide-square')?.closest('button');
|
||||
// The checkbox is the button marked packing-check
|
||||
const toggleBtn = container.querySelector<HTMLButtonElement>('.packing-check');
|
||||
expect(toggleBtn).toBeTruthy();
|
||||
await user.click(toggleBtn!);
|
||||
|
||||
@@ -861,9 +861,9 @@ describe('PackingListPanel', () => {
|
||||
// In jsdom, CSS classes don't apply so the buttons are accessible
|
||||
// The dot button has a circle span inside with category color
|
||||
// Find all buttons with the 'Move to List' title
|
||||
const catChangeBtn = screen.getAllByTitle('Move to List');
|
||||
expect(catChangeBtn.length).toBeGreaterThan(0);
|
||||
await user.click(catChangeBtn[0]);
|
||||
// Moving an item goes through its menu: the three dots, then Move to List.
|
||||
await user.click(container.querySelector<HTMLButtonElement>('.packing-row-overflow button')!);
|
||||
await user.click(screen.getByText('Move to List'));
|
||||
|
||||
// Category picker shows both category names
|
||||
await waitFor(() => {
|
||||
@@ -1059,11 +1059,11 @@ describe('PackingListPanel', () => {
|
||||
return HttpResponse.json({ item: buildPackingItem({ id: 74, category: 'Documents' }) });
|
||||
})
|
||||
);
|
||||
render(<PackingListPanel tripId={1} items={[itemA, itemB]} />);
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[itemA, itemB]} />);
|
||||
|
||||
// Use fireEvent (no pointer events) to open the category picker — avoids mouseLeave closing picker
|
||||
const catChangeBtns = screen.getAllByTitle('Move to List');
|
||||
fireEvent.click(catChangeBtns[0]);
|
||||
// Open the first item's menu and its Move to List entry
|
||||
fireEvent.click(container.querySelector<HTMLButtonElement>('.packing-row-overflow button')!);
|
||||
fireEvent.click(screen.getByText('Move to List'));
|
||||
|
||||
// Picker shows available categories — find and click the 'Documents' button (role=button, text=Documents)
|
||||
const docBtn = await screen.findByRole('button', { name: 'Documents' });
|
||||
@@ -1248,9 +1248,10 @@ describe('PackingListPanel', () => {
|
||||
const items = [buildPackingItem({ name: 'Boots', category: 'Clothing' })];
|
||||
render(<PackingListPanel tripId={1} items={items} />);
|
||||
|
||||
// Wait for sidebar "Add bag" button (sidebar renders when bags.length > 0)
|
||||
await waitFor(() => expect(screen.getAllByText('Add bag').length).toBeGreaterThan(0));
|
||||
const addBagBtns = screen.getAllByText('Add bag');
|
||||
// Wait for the sidebar's add button (sidebar renders when bags.length > 0); it
|
||||
// reads just "Bags" and carries "Add bag" as its accessible name.
|
||||
await waitFor(() => expect(screen.getAllByRole('button', { name: 'Add bag' }).length).toBeGreaterThan(0));
|
||||
const addBagBtns = screen.getAllByRole('button', { name: 'Add bag' });
|
||||
await user.click(addBagBtns[0]);
|
||||
|
||||
// Bag name input appears
|
||||
@@ -1637,7 +1638,55 @@ describe('PackingListPanel', () => {
|
||||
render(<PackingListPanel tripId={1} items={items} />);
|
||||
await userEvent.click(screen.getByText('My list'));
|
||||
await screen.findByText('Power bank');
|
||||
// "by Bob" — taken care of by the bringer.
|
||||
expect(screen.getByText('by Bob')).toBeInTheDocument();
|
||||
// "by Bob" — taken care of by the bringer, as the chip's tooltip and label.
|
||||
expect(screen.getByLabelText('by Bob')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PackingListPanel — under the Lists bar', () => {
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
seedStore(useAuthStore, { user: buildAdmin({ id: 1 }), isAuthenticated: true });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1 }) });
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-090: the clean-up sits in the progress card and clears what is ticked', async () => {
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
let deleted = false;
|
||||
server.use(http.delete('/api/trips/1/packing/91', () => { deleted = true; return HttpResponse.json({ success: true }); }));
|
||||
render(<PackingListPanel tripId={1} inlineHeader={false} items={[buildPackingItem({ id: 91, name: 'Towel', checked: 1 }), buildPackingItem({ id: 92, name: 'Soap' })]} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove 1 checked' }));
|
||||
await waitFor(() => expect(deleted).toBe(true));
|
||||
// No dashed Add list under the bar: the Lists bar carries it.
|
||||
expect(screen.queryByRole('button', { name: 'Add list' })).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-091: a raised add-list signal opens the name dialog, which creates the list', async () => {
|
||||
let body: Record<string, unknown> | null = null;
|
||||
server.use(http.post('/api/trips/1/packing', async ({ request }) => {
|
||||
body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ item: buildPackingItem({ id: 93, name: '...', category: 'Rain kit' }) });
|
||||
}));
|
||||
const { rerender } = render(<PackingListPanel tripId={1} inlineHeader={false} items={[buildPackingItem({ id: 92, name: 'Soap' })]} addCategorySignal={0} />);
|
||||
rerender(<PackingListPanel tripId={1} inlineHeader={false} items={[buildPackingItem({ id: 92, name: 'Soap' })]} addCategorySignal={1} />);
|
||||
|
||||
fireEvent.change(await screen.findByPlaceholderText('List name (e.g. Clothing)'), { target: { value: 'Rain kit' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
await waitFor(() => expect(body).toMatchObject({ category: 'Rain kit' }));
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-092: Save as template asks for the name in a dialog', async () => {
|
||||
let body: Record<string, unknown> | null = null;
|
||||
server.use(http.post('/api/trips/1/packing/save-as-template', async ({ request }) => {
|
||||
body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ template: { id: 1, name: 'Beach' } });
|
||||
}));
|
||||
const { rerender } = render(<PackingListPanel tripId={1} inlineHeader={false} items={[buildPackingItem({ id: 92, name: 'Soap' })]} saveTemplateSignal={0} />);
|
||||
rerender(<PackingListPanel tripId={1} inlineHeader={false} items={[buildPackingItem({ id: 92, name: 'Soap' })]} saveTemplateSignal={1} />);
|
||||
|
||||
fireEvent.change(await screen.findByPlaceholderText('Template name'), { target: { value: 'Beach' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
await waitFor(() => expect(body).toMatchObject({ name: 'Beach' }));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user