mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
b598b97865
The phone trip sheet host wrote out by hand three pieces of planner wiring the desktop planner already had. useBookingExpenseEditor now holds the expense editor both planners open over the trip: the booking request state, the receipt fallback, the base currency, the current user and the save that reloads the budget. TripPlannerPage and MTripSheets both read it. applyTripCoverUpdate patches a new trip cover into the store for the trip form on both sides. The transport editor and the transit route change moved into openTransportEditorWith and changeTransitRouteWith, which take the planner's setters. usePlannerDialogs builds openTransportEditor and changeTransitRoute on them, and the phone sheet calls the same helpers on the planner it receives. There are no differences between the two views to keep as options.
353 lines
13 KiB
TypeScript
353 lines
13 KiB
TypeScript
import { vi } from 'vitest';
|
|
import type { TripPlanner, MTripShellApi } from '../../src/mobile/screens/trip/MTripShell';
|
|
import type { Reservation } from '../../src/types';
|
|
import { changeTransitRouteWith, openTransportEditorWith } from '../../src/pages/tripPlanner/transportEditorOpeners';
|
|
import { buildTrip } from './factories';
|
|
|
|
/**
|
|
* Fixtures for the mobile trip screens. Every tab panel, sheet and overlay
|
|
* under src/mobile/screens/trip receives the same two props — the planner
|
|
* context from useTripPlanner() and the shell api from MTripShell — so tests
|
|
* render them directly with these builders instead of mounting the whole shell.
|
|
*
|
|
* Defaults are the empty-but-valid state: a trip exists, every list is empty,
|
|
* every callback is a spy. Pass overrides for the slice under test.
|
|
*/
|
|
|
|
/** Mirrors useTripStore's action surface as spies. */
|
|
export function buildTripActions(): Record<string, ReturnType<typeof vi.fn>> {
|
|
const names = [
|
|
'addCategory', 'addDayNote', 'addFile', 'addPackingContributor', 'addPackingItem',
|
|
'addPlace', 'addReservation', 'addTodoItem', 'appendDatedDay', 'assignPlaceToDay', 'clonePackingItem',
|
|
'deleteBudgetItem', 'deleteDayNote', 'deleteFile', 'deletePackingItem', 'deletePlace',
|
|
'deleteDay', 'deletePlacesMany', 'deleteReservation', 'deleteTodoItem', 'insertDay', 'loadBudgetItems',
|
|
'loadFiles', 'loadReservations', 'loadTrip', 'moveAssignment', 'moveDayNote', 'ratePlace',
|
|
'refreshDays', 'removeAssignment', 'removePackingContributor', 'reorderAssignments',
|
|
'reorderDays', 'setAssignments', 'setPackedCount', 'setPackingItemSharing', 'setSelectedDay',
|
|
'toggleBudgetMemberPaid', 'togglePackingItem', 'toggleReservationStatus', 'toggleTodoItem',
|
|
'updateDayNote', 'updateDayTitle', 'updatePackingItem', 'updatePlace', 'updatePlacesMany',
|
|
'updateReservation', 'updateTodoItem', 'updateTrip', 'uploadPlaceImage',
|
|
];
|
|
const actions: Record<string, ReturnType<typeof vi.fn>> = {};
|
|
for (const name of names) actions[name] = vi.fn(async () => undefined);
|
|
return actions;
|
|
}
|
|
|
|
export function buildToast(): Record<string, ReturnType<typeof vi.fn>> {
|
|
return { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() };
|
|
}
|
|
|
|
/**
|
|
* `t` echoes the key so assertions stay independent of the English copy.
|
|
* Pass a real translation fn through overrides when a test needs visible text.
|
|
*/
|
|
const echoT = (key: string, params?: Record<string, string | number>): string =>
|
|
params ? `${key}:${Object.values(params).join(',')}` : key;
|
|
|
|
export function buildPlanner(overrides: Partial<TripPlanner> = {}): TripPlanner {
|
|
const trip = buildTrip({ id: 1, title: 'Japan 2026', currency: 'EUR' });
|
|
|
|
const base = {
|
|
tripId: 1,
|
|
navigate: vi.fn(),
|
|
toast: buildToast(),
|
|
t: echoT,
|
|
language: 'en',
|
|
settings: { time_format: '24h', date_format: 'DD.MM.YYYY', default_currency: 'EUR', distance_unit: 'km' },
|
|
placesPhotosEnabled: false,
|
|
|
|
trip,
|
|
days: [],
|
|
places: [],
|
|
assignments: [],
|
|
// The unfiltered map the store holds. The place editor looks the visit it was
|
|
// opened on up here, so every sheet test that edits a place needs the shape.
|
|
storedAssignments: {},
|
|
packingItems: [],
|
|
todoItems: [],
|
|
categories: [],
|
|
reservations: [],
|
|
budgetItems: [],
|
|
files: [],
|
|
|
|
selectedDayId: null,
|
|
isLoading: false,
|
|
tripActions: buildTripActions(),
|
|
can: vi.fn(() => true),
|
|
canUploadFiles: true,
|
|
isTourPlace: vi.fn(() => false),
|
|
|
|
pushUndo: vi.fn(),
|
|
undo: vi.fn(),
|
|
canUndo: false,
|
|
lastActionLabel: null,
|
|
handleUndo: vi.fn(),
|
|
|
|
enabledAddons: { packing: true, budget: true, documents: true, collab: true, vacay: true, atlas: true },
|
|
collabFeatures: { chat: true, notes: true, polls: true },
|
|
tripAccommodations: [],
|
|
setTripAccommodations: vi.fn(),
|
|
allowedFileTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
tripMembers: [],
|
|
setTripMembers: vi.fn(),
|
|
refreshMembers: vi.fn(),
|
|
loadAccommodations: vi.fn(),
|
|
|
|
TRANSPORT_TYPES: new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other']),
|
|
TRIP_TABS: [],
|
|
activeTab: 'plan',
|
|
setActiveTab: vi.fn(),
|
|
handleTabChange: vi.fn(),
|
|
|
|
leftWidth: 320,
|
|
rightWidth: 320,
|
|
leftCollapsed: false,
|
|
rightCollapsed: false,
|
|
setLeftCollapsed: vi.fn(),
|
|
setRightCollapsed: vi.fn(),
|
|
startResizeLeft: vi.fn(),
|
|
startResizeRight: vi.fn(),
|
|
|
|
selectedPlaceId: null,
|
|
selectedAssignmentId: null,
|
|
setSelectedPlaceId: vi.fn(),
|
|
selectAssignment: vi.fn(),
|
|
|
|
showDayDetail: false,
|
|
setShowDayDetail: vi.fn(),
|
|
dayDetailCollapsed: false,
|
|
setDayDetailCollapsed: vi.fn(),
|
|
showPlaceForm: false,
|
|
setShowPlaceForm: vi.fn(),
|
|
editingPlace: null,
|
|
setEditingPlace: vi.fn(),
|
|
prefillCoords: null,
|
|
setPrefillCoords: vi.fn(),
|
|
placeFormDayId: null,
|
|
setPlaceFormDayId: vi.fn(),
|
|
reservationModalDayId: null,
|
|
setReservationModalDayId: vi.fn(),
|
|
editingAssignmentId: null,
|
|
setEditingAssignmentId: vi.fn(),
|
|
showTripForm: false,
|
|
setShowTripForm: vi.fn(),
|
|
showMembersModal: false,
|
|
setShowMembersModal: vi.fn(),
|
|
showReservationModal: false,
|
|
setShowReservationModal: vi.fn(),
|
|
editingReservation: null,
|
|
setEditingReservation: vi.fn(),
|
|
showBookingImport: false,
|
|
setShowBookingImport: vi.fn(),
|
|
bookingImportKind: 'bookings' as const,
|
|
setBookingImportKind: vi.fn(),
|
|
bookingImportAvailable: true,
|
|
airTrailAvailable: false,
|
|
showAirTrailImport: false,
|
|
setShowAirTrailImport: vi.fn(),
|
|
bookingForAssignmentId: null,
|
|
setBookingForAssignmentId: vi.fn(),
|
|
|
|
showTransportModal: false,
|
|
setShowTransportModal: vi.fn(),
|
|
editingTransport: null,
|
|
setEditingTransport: vi.fn(),
|
|
transportModalDayId: null,
|
|
setTransportModalDayId: vi.fn(),
|
|
transportModalAutomated: false,
|
|
setTransportModalAutomated: vi.fn(),
|
|
transitPrefill: null,
|
|
setTransitPrefill: vi.fn(),
|
|
transitJourney: null,
|
|
setTransitJourney: vi.fn(),
|
|
reservationPrefill: null,
|
|
transportPrefill: null,
|
|
importReviewActive: false,
|
|
startImportReview: vi.fn(),
|
|
advanceImportReview: vi.fn(),
|
|
|
|
routeShown: false,
|
|
setRouteShown: vi.fn(),
|
|
autoShowRoute: vi.fn(),
|
|
routeProfile: 'driving',
|
|
setRouteProfile: vi.fn(),
|
|
routeVias: [],
|
|
// Whether the day ends at a stop, from the flag and from a manual boundary alike. The
|
|
// helper answers from the flag; a case about boundaries overrides it.
|
|
roadtripEndsDayAt: (stop: { endDay?: boolean } | null | undefined) => !!stop?.endDay,
|
|
fitKey: 0,
|
|
setFitKey: vi.fn(),
|
|
|
|
mobileSidebarOpen: false,
|
|
setMobileSidebarOpen: vi.fn(),
|
|
mobilePlanScrollTopRef: { current: 0 },
|
|
mobilePlacesScrollTopRef: { current: 0 },
|
|
|
|
deletePlaceId: null,
|
|
setDeletePlaceId: vi.fn(),
|
|
deletePlaceIds: null,
|
|
setDeletePlaceIds: vi.fn(),
|
|
|
|
// Adding a day from the days sheet: a trip without dates, so the single button.
|
|
dayAdd: { nextDate: null, blocked: null, datedBlocked: null, busy: false, onAddDated: vi.fn() },
|
|
|
|
// Deleting a day from the days sheet: no question open, nothing blocking.
|
|
deleteDayId: null,
|
|
setDeleteDayId: vi.fn(),
|
|
deleteDayTitle: '',
|
|
deleteDayLines: [],
|
|
deleteDayBlocked: null,
|
|
handleDeleteDay: vi.fn(),
|
|
confirmDeleteDay: vi.fn(async () => undefined),
|
|
|
|
// resolveVisibleConnectionIds returns an array, and consumers call .includes().
|
|
visibleConnections: [] as number[],
|
|
roadtripConnections: [] as number[],
|
|
toggleConnection: vi.fn(),
|
|
allConnectionsShown: false,
|
|
toggleAllConnections: vi.fn(),
|
|
mapTransportDetail: null,
|
|
setMapTransportDetail: vi.fn(),
|
|
|
|
isMobile: true,
|
|
isTouch: true,
|
|
|
|
expandedDayIds: new Set<number>(),
|
|
setExpandedDayIds: vi.fn(),
|
|
mapPlaces: [],
|
|
// The recorded-route overlay (#2279): off and empty. MMapArea reads
|
|
// `dawarichTrail.track` unconditionally, so the shape has to be here even
|
|
// when the addon is not in play.
|
|
dawarichEnabled: false,
|
|
dawarichTrailShown: false,
|
|
toggleDawarichTrail: vi.fn(),
|
|
dawarichTrail: { track: null, status: 'idle', reload: vi.fn() },
|
|
|
|
// The road trip addon's routing round. The shell reads days/totalDistance for
|
|
// its stage header on every render, addon on or off, so the empty-but-valid
|
|
// shape belongs in the base rather than in the overrides of one suite.
|
|
roadtripRoutes: {
|
|
days: [], quietDays: [], lines: [], lineDays: [], accessLines: [], vias: [], segments: [],
|
|
totalDistance: 0, totalDuration: 0, totalStops: 0, loading: false,
|
|
},
|
|
|
|
// The corridor search, idle. Both the search bar over the stage and the search
|
|
// sheet read it on every render of the road trip tab, addon on or off, so the
|
|
// empty-but-valid shape belongs here rather than in one suite's overrides.
|
|
roadtripCorridor: {
|
|
dayId: '', setDayId: vi.fn(), day: undefined,
|
|
categories: ['fuel'], toggleCategory: vi.fn(),
|
|
widthKm: 5, setWidthKm: vi.fn(),
|
|
search: {
|
|
results: [], progress: { done: 0, total: 0 }, loading: false, capped: false,
|
|
failedSources: [], failedAreas: 0, truncatedAreas: 0, error: false, spine: [],
|
|
search: vi.fn(), clear: vi.fn(),
|
|
},
|
|
nameFilter: '', setNameFilter: vi.fn(), anchors: [],
|
|
section: null, setSection: vi.fn(), sectionKm: 50, setSectionKm: vi.fn(),
|
|
socketFilter: '', setSocketFilter: vi.fn(), minKw: 0, setMinKw: vi.fn(),
|
|
visible: [], insertIndexFor: vi.fn(() => 0), stopsAlongKm: [], clear: vi.fn(),
|
|
},
|
|
// The fuel search, idle: the dry band in the chain reads it per leg.
|
|
refuel: {
|
|
openFor: null, loading: false, outcome: null, results: [], offered: [],
|
|
ask: vi.fn(), close: vi.fn(),
|
|
},
|
|
askRefuel: vi.fn(),
|
|
acceptRefuel: vi.fn(),
|
|
// Other ways of driving a leg, with no picker open. The road trip tab and the map area
|
|
// read the picker on every render, so the closed shape belongs in the base.
|
|
routeAlternatives: { open: null, ask: vi.fn(), close: vi.fn() },
|
|
askRouteAlternatives: vi.fn(),
|
|
chooseRouteAlternative: vi.fn(async () => undefined),
|
|
alternativeOverlays: [],
|
|
alternativeFocusPoints: [],
|
|
highlightedAlternative: null,
|
|
setHighlightedAlternative: vi.fn(),
|
|
// The drive's vias, online and empty. `editable` is what the leg buttons read.
|
|
roadtripVias: {
|
|
byDay: {}, trackByDay: {}, stale: false, editable: true,
|
|
add: vi.fn(), addMany: vi.fn(), move: vi.fn(), remove: vi.fn(), reanchor: vi.fn(),
|
|
},
|
|
mapFocusPoints: [],
|
|
focusRoadtripPoint: vi.fn(),
|
|
handlePoiClick: vi.fn(),
|
|
|
|
route: null,
|
|
routeSegments: [],
|
|
routeInfo: null,
|
|
setRoute: vi.fn(),
|
|
setRouteInfo: vi.fn(),
|
|
updateRouteForDay: vi.fn(),
|
|
|
|
handleSelectDay: vi.fn(),
|
|
handlePlaceClick: vi.fn(),
|
|
handleMarkerClick: vi.fn(),
|
|
handleMapClick: vi.fn(),
|
|
handleMapContextMenu: vi.fn(),
|
|
openAddPlaceFromPoi: vi.fn(),
|
|
handleSavePlace: vi.fn(),
|
|
openPlaceEditor: vi.fn(),
|
|
handleDeletePlace: vi.fn(),
|
|
confirmDeletePlace: vi.fn(),
|
|
confirmDeletePlaces: vi.fn(),
|
|
confirmChangeCategory: vi.fn(),
|
|
handleAssignToDay: vi.fn(),
|
|
handleRemoveAssignment: vi.fn(),
|
|
handleReorder: vi.fn(),
|
|
handleReorderDays: vi.fn(),
|
|
handleAddDay: vi.fn(),
|
|
handleUpdateDayTitle: vi.fn(),
|
|
handleSaveReservation: vi.fn(),
|
|
handleSaveTransport: vi.fn(),
|
|
handleDeleteReservation: vi.fn(),
|
|
// The planner's two ways into the transport editor, run on the setter spies above
|
|
// the way usePlannerDialogs runs them on its state.
|
|
openTransportEditor: vi.fn((r: Reservation) => openTransportEditorWith(planner, r)),
|
|
changeTransitRoute: vi.fn((r: Reservation) => changeTransitRouteWith(planner, r)),
|
|
|
|
selectedPlace: null,
|
|
dayOrderMap: new Map<number, number>(),
|
|
dayPlaces: [],
|
|
|
|
mapTileUrl: 'https://tile.example/{z}/{x}/{y}.png',
|
|
fontStyle: {},
|
|
splashDone: true,
|
|
};
|
|
|
|
const planner = { ...base, ...overrides } as unknown as TripPlanner;
|
|
return planner;
|
|
}
|
|
|
|
export function buildShell(overrides: Partial<MTripShellApi> = {}): MTripShellApi {
|
|
const base: MTripShellApi = {
|
|
view: 'plan',
|
|
rtView: 'list',
|
|
mapFront: false,
|
|
toggleRtView: vi.fn(),
|
|
rtReach: 'ahead',
|
|
setRtReach: vi.fn(),
|
|
mode: 'go',
|
|
trTab: 'plan',
|
|
setTrTab: vi.fn(),
|
|
setTravelMode: vi.fn(),
|
|
toggleView: vi.fn(),
|
|
browseFromEdit: false,
|
|
sheet: null,
|
|
openSheet: vi.fn(),
|
|
closeSheet: vi.fn(),
|
|
listsTab: 'packing',
|
|
setListsTab: vi.fn(),
|
|
collabTab: 'chat',
|
|
setCollabTab: vi.fn(),
|
|
transportsCompact: false,
|
|
bookingsCompact: false,
|
|
addExpenseSignal: 0,
|
|
exportCostsCsvSignal: 0,
|
|
uploadFilesSignal: 0,
|
|
openFilesTrashSignal: 0,
|
|
openDocSyncSignal: 0,
|
|
};
|
|
return { ...base, ...overrides };
|
|
}
|