test(client): expand frontend test suite to 69.1% coverage

Add and extend tests across 32 files (+10 595 lines) covering Admin
panels (AuditLog, Backup, DevNotifications, GitHub), Collab (Chat,
Notes, Panel, Polls), Planner (DayDetailPanel, DayPlanSidebar),
Settings (DisplaySettings, Integrations, MapSettings), Files
(FileManager, FilesPage), Map, Layout (DemoBanner,
InAppNotificationBell), shared pickers (CustomDateTimePicker,
CustomTimePicker), Vacay holidays, pages (Dashboard, Login), unit
stores (authStore, inAppNotificationStore), API (authUrl, client
integration), and i18n. Also updates sonar-project.properties and
MSW trip handlers to support the new cases.
This commit is contained in:
jubnl
2026-04-07 21:55:41 +02:00
parent 9390a2e9c6
commit fd48169219
32 changed files with 10595 additions and 15 deletions
@@ -238,4 +238,184 @@ describe('BudgetPanel', () => {
render(<BudgetPanel tripId={1} tripMembers={[]} />);
await screen.findByText('No budget created yet');
});
it('FE-COMP-BUDGET-021: inline edit name cell — clicking a name cell makes it editable', async () => {
const user = userEvent.setup();
const item = { ...buildBudgetItem({ id: 21, trip_id: 1, category: 'Food', name: 'Old Name' }), total_price: 10 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('Old Name');
await user.click(screen.getByText('Old Name'));
expect(screen.getByDisplayValue('Old Name')).toBeInTheDocument();
await user.keyboard('{Escape}');
expect(screen.queryByDisplayValue('Old Name')).not.toBeInTheDocument();
});
it('FE-COMP-BUDGET-022: inline edit name cell — saving new name calls PUT API', async () => {
const user = userEvent.setup();
const item = { ...buildBudgetItem({ id: 10, trip_id: 1, category: 'Food', name: 'Old Name' }), total_price: 10 };
let putCalled = false;
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.put('/api/trips/1/budget/10', async ({ request }) => {
const b = await request.json() as Record<string, unknown>;
putCalled = true;
return HttpResponse.json({ item: { ...item, name: b.name } });
})
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('Old Name');
await user.click(screen.getByText('Old Name'));
const input = screen.getByDisplayValue('Old Name');
await user.clear(input);
await user.type(input, 'New Name');
await user.tab();
await waitFor(() => expect(putCalled).toBe(true));
});
it('FE-COMP-BUDGET-023: total price is shown formatted with currency symbol', async () => {
const item = { ...buildBudgetItem({ id: 23, trip_id: 1, category: 'Restaurants', name: 'Dinner' }), total_price: 45.5 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('Dinner');
// The formatted number appears in the InlineEditCell for total price (and grand total card)
expect(screen.getAllByText('45.50').length).toBeGreaterThan(0);
// The currency symbol appears (in category subtotal or grand total card)
expect(screen.getAllByText(/€/).length).toBeGreaterThan(0);
});
it('FE-COMP-BUDGET-024: delete category button removes all items in that category', async () => {
const user = userEvent.setup();
const item = { ...buildBudgetItem({ id: 24, trip_id: 1, category: 'Flights', name: 'Flight to Paris' }), total_price: 200 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.delete('/api/trips/1/budget/24', () => HttpResponse.json({ success: true }))
);
render(<BudgetPanel tripId={1} />);
await screen.findAllByText('Flights');
await screen.findByText('Flight to Paris');
await user.click(screen.getByTitle('Delete Category'));
await waitFor(() => {
expect(screen.queryAllByText('Flights').length).toBe(0);
expect(screen.queryByText('Flight to Paris')).not.toBeInTheDocument();
});
});
it('FE-COMP-BUDGET-025: CSV export button triggers download via URL.createObjectURL', async () => {
const createObjectURL = vi.fn(() => 'blob:test');
vi.spyOn(URL, 'createObjectURL').mockImplementation(createObjectURL);
const user = userEvent.setup();
const item = { ...buildBudgetItem({ trip_id: 1, category: 'Other', name: 'Misc' }), total_price: 10 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('CSV');
await user.click(screen.getByText('CSV'));
expect(createObjectURL).toHaveBeenCalled();
vi.restoreAllMocks();
});
it('FE-COMP-BUDGET-026: category total row shows sum of items in category', async () => {
const item1 = { ...buildBudgetItem({ trip_id: 1, category: 'Food', name: 'Lunch' }), total_price: 20 };
const item2 = { ...buildBudgetItem({ trip_id: 1, category: 'Food', name: 'Dinner' }), total_price: 30 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item1, item2] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('Lunch');
// The category header shows subtotal formatted as "50.00 €" (also appears in pie legend)
expect(screen.getAllByText('50.00 €').length).toBeGreaterThan(0);
});
it('FE-COMP-BUDGET-027: add new category input is visible in empty state', async () => {
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByPlaceholderText('Enter category name...');
});
it('FE-COMP-BUDGET-028: creating a new category via input calls POST and adds a section', async () => {
const user = userEvent.setup();
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })),
http.post('/api/trips/1/budget', () =>
HttpResponse.json({ item: { ...buildBudgetItem({ category: 'Souvenirs', name: 'New Entry' }), total_price: 0 } })
)
);
render(<BudgetPanel tripId={1} />);
const input = await screen.findByPlaceholderText('Enter category name...');
await user.type(input, 'Souvenirs{Enter}');
await screen.findByText('Souvenirs');
});
it('FE-COMP-BUDGET-029: settlement section renders flows with usernames', async () => {
const user = userEvent.setup();
const item = { ...buildBudgetItem({ trip_id: 1, category: 'Food', name: 'Dinner' }), total_price: 100 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.get('/api/trips/1/budget/settlement', () =>
HttpResponse.json({
balances: [
{ user_id: 1, username: 'alice', balance: -10, avatar_url: null },
{ user_id: 2, username: 'bob', balance: 10, avatar_url: null },
],
flows: [
{ from: { username: 'alice', avatar_url: null }, to: { username: 'bob', avatar_url: null }, amount: 10 },
],
})
)
);
const tripMembers = [
{ id: 1, username: 'alice', avatar_url: null },
{ id: 2, username: 'bob', avatar_url: null },
];
render(<BudgetPanel tripId={1} tripMembers={tripMembers} />);
await screen.findByText('Dinner');
// Click the settlement toggle button (role button with name containing "settlement")
const settlementBtn = await screen.findByRole('button', { name: /settlement/i });
await user.click(settlementBtn);
// alice and bob should appear in balances section
await screen.findByText('alice');
await screen.findByText('bob');
});
it('FE-COMP-BUDGET-030: per-person summary renders usernames', async () => {
const item = {
...buildBudgetItem({ trip_id: 1, category: 'Food', name: 'Shared Dinner' }),
total_price: 75,
members: [{ user_id: 1, username: 'testuser', avatar_url: null, paid: false }],
};
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })),
http.get('/api/trips/1/budget/summary/per-person', () =>
HttpResponse.json({ summary: [{ user_id: 1, username: 'testuser', avatar_url: null, total_assigned: 75 }] })
)
);
const tripMembers = [
{ id: 1, username: 'testuser', avatar_url: null },
{ id: 2, username: 'other', avatar_url: null },
];
render(<BudgetPanel tripId={1} tripMembers={tripMembers} />);
await screen.findByText('Shared Dinner');
await screen.findByText('testuser');
});
it('FE-COMP-BUDGET-032: grand total row shows sum across all categories', async () => {
const item1 = { ...buildBudgetItem({ trip_id: 1, category: 'Transport', name: 'Flight' }), total_price: 100 };
const item2 = { ...buildBudgetItem({ trip_id: 1, category: 'Hotels', name: 'Hotel' }), total_price: 200 };
server.use(
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item1, item2] }))
);
render(<BudgetPanel tripId={1} />);
await screen.findByText('Flight');
await screen.findByText('Hotel');
// Grand total card shows 300.00
expect(screen.getByText('300.00')).toBeInTheDocument();
});
});