fix(planner): an imported booking with source files saves and opens its expense without file support

Saving an imported booking in the desktop booking dialog when the dialog
had no upload handler but the import carried source files rejected the
save: the files were handed to the missing handler, the promise rejected
with nothing to catch it, and the expense editor the user asked for never
opened. The transport dialog already skipped the upload in that case, the
booking dialog did not.

The shared uploadBookingFiles helper now takes an optional handler and
does nothing without one, so every form gets the same guard.

Red test: FE-PLANNER-RESMODAL-106 in
client/src/components/Planner/ReservationModal.test.tsx failed with
"expected spy to be called with arguments" for onOpenExpense, plus the
unhandled rejection "TypeError: upload is not a function". The helper
case is covered by FE-PLANNER-BOOKINGFORM-008.

No existing assertions changed.
This commit is contained in:
Maurice
2026-10-09 17:36:59 +02:00
parent 07117949ef
commit 70f4f7cef3
3 changed files with 42 additions and 3 deletions
@@ -1,4 +1,4 @@
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-105
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-106
import { render, screen, waitFor, fireEvent, within, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -1409,6 +1409,38 @@ describe('ReservationModal', () => {
expect(onSave.mock.calls[0][0].create_budget_entry).toEqual({ total_price: 50, category: 'accommodation' });
});
it('FE-PLANNER-RESMODAL-106: an import with source files but no upload handler still saves and opens the expense editor', async () => {
// Without onFileUpload the pending source files cannot go anywhere; the save
// used to call the missing handler, reject, and never reach the expense step.
budgetEnabled();
const onSave = vi.fn().mockResolvedValue({ id: 84 });
const onOpenExpense = vi.fn();
const prefill = hotelPrefill({
type: 'restaurant',
metadata: {},
_accommodation: undefined,
_sourceFiles: [new File(['pdf'], 'voucher.pdf', { type: 'application/pdf' })],
});
render(
<ReservationModal
{...defaultProps}
onFileUpload={undefined}
onSave={onSave}
onOpenExpense={onOpenExpense}
prefill={prefill}
days={reviewDays()}
/>
);
await userEvent.click(screen.getByRole('button', { name: /Create expense/i }));
await waitFor(() => expect(onSave).toHaveBeenCalled());
await waitFor(() =>
expect(onOpenExpense).toHaveBeenCalledWith(
expect.objectContaining({ prefill: expect.objectContaining({ reservationId: 84 }) })
)
);
});
// ── File error paths ────────────────────────────────────────────────────────
it('FE-PLANNER-RESMODAL-077: a cancelled file dialog changes nothing', () => {
@@ -1,4 +1,4 @@
// FE-PLANNER-BOOKINGFORM-001 to -007: the travellers, picked files, attached files
// FE-PLANNER-BOOKINGFORM-001 to -008: the travellers, picked files, attached files
// and expense requests of the transport and booking forms, desktop and phone alike.
import { typeToCostCategory } from '@trek/shared';
import { describe, expect, it, vi } from 'vitest';
@@ -60,6 +60,10 @@ describe('bookingFormModel', () => {
expect(upload).not.toHaveBeenCalled();
});
it('FE-PLANNER-BOOKINGFORM-008: without an upload handler the picked files are skipped and the save is not rejected', async () => {
await expect(uploadBookingFiles(undefined, 42, [new File(['a'], 'a.pdf')], 'Hotel stay')).resolves.toBeUndefined();
});
it('FE-PLANNER-BOOKINGFORM-005: an expense wish opens the editor for the saved booking, nothing else does', () => {
const booking = { title: 'Hotel Adlon', type: 'hotel' };
const item = buildBudgetItem({ id: 7 });
@@ -34,13 +34,16 @@ export function travelersChanged(reservation: Pick<Reservation, 'travelers'> | n
* the other, each described with the booking's title. Callers run it after both a
* create and an edit: the form only holds files the user just picked, so nothing is
* uploaded twice, and skipping the edit dropped them without a word (#2534).
* Without an upload handler (a dialog opened with no file support) there is
* nowhere to send them, so nothing is uploaded and the save carries on.
*/
export async function uploadBookingFiles(
upload: (fd: FormData) => Promise<unknown>,
upload: ((fd: FormData) => Promise<unknown>) | undefined,
savedId: number,
files: File[],
description: string
): Promise<void> {
if (!upload) return;
for (const file of files) {
const fd = new FormData();
fd.append('file', file);