mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
92ccd24eed
**A saved stay left an invisible sheet standing, and the day swipe stayed dead behind it.** The stay sheet reopened 'rtstop' by spreading its own payload, which carries a place id. That sheet is located by day and assignment, so it resolved to nothing and drew nothing, while `shell.sheet` still pointed at it and `menuOpen: !!shell.sheet` kept the day swipe blocked until something else closed it. The row rides along now, both ways, and with no row to go back to the sheet closes rather than opening one that resolves to nothing. **"End the day here" showed off for a day that already ends there, and switching it on deleted that day end.** Two things close a day: the stop's own flag and a manual boundary filed against it. The writer has always known about both; the phone switch read only the flag, so the tap ran through the writer's boundary branch and cleared the boundary. The question is asked once now, in the planner beside that writer, and the desktop reads the same answer it always computed inline. **The road trip chain printed 24-hour clocks at a reader on a twelve-hour one.** The model hands clocks over as HH:MM, which is what the schedule computes in, and the chain, the head card and the up-next card printed it straight. The desktop rail, the day timeline and this tab's own sheets all honour the setting; these go through the same formatter, carried in the row chrome so no two rows can disagree. **The up-next card's navigate button read "places.navigate".** No locale has that key. It is `inspector.navigation`, which is what the stop sheet beside it, the place inspector and the place sheet all use, with the single-target label when there is only one. **Offered roads were invisible on a satellite basemap.** The imagery is anchored under the first layer TREK draws, found by prefix, and the roads offered for a leg are added before the route source so they sit under the current route. That also puts them before the first `trip-` layer, so the raster went straight over them. The prefix list knows them now. **A journal grew a day section for every day Dawarich recorded, all year.** The hook reads every open stay the account has, and the timeline took all of their dates. A week in May therefore sprouted sections from September. Bounded by the journal's own span now, its entries together with the dates of the trips it links, and with no span at all it offers nothing rather than everything.
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
import { vi } from 'vitest';
|
|
import type { TripPlanner, MTripShellApi } from '../../src/mobile/screens/trip/MTripShell';
|
|
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', 'assignPlaceToDay', 'clonePackingItem',
|
|
'deleteBudgetItem', 'deleteDayNote', 'deleteFile', 'deletePackingItem', 'deletePlace',
|
|
'deletePlacesMany', 'deleteReservation', 'deleteTodoItem', 'insertDay', 'loadBudgetItems',
|
|
'loadFiles', 'loadReservations', 'loadTrip', 'moveAssignment', 'moveDayNote', 'ratePlace',
|
|
'refreshDays', 'removeAssignment', 'removePackingContributor', 'reorderAssignments',
|
|
'reorderDays', 'setAssignments', '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,
|
|
|
|
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(),
|
|
|
|
// resolveVisibleConnectionIds returns an array, and consumers call .includes().
|
|
visibleConnections: [] 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(),
|
|
|
|
selectedPlace: null,
|
|
dayOrderMap: new Map<number, number>(),
|
|
dayPlaces: [],
|
|
|
|
mapTileUrl: 'https://tile.example/{z}/{x}/{y}.png',
|
|
fontStyle: {},
|
|
splashDone: true,
|
|
};
|
|
|
|
return { ...base, ...overrides } as unknown as TripPlanner;
|
|
}
|
|
|
|
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,
|
|
};
|
|
return { ...base, ...overrides };
|
|
}
|