mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
1430056d24
The attempt counter and the backoff were written on every failure and never consulted when planning the next run: rows were found in a live database at 46 attempts against a limit of 6, so a document a provider refuses — one it reads as corrupt, a type it will not take, a full quota — was re-uploaded on every cron tick forever. planReconcile now skips a row that is shelved or still inside its window, and a manual run clears the counters, which is what "waits for a person" has to mean. The webhook had the same shape of bug: WEBHOOK_NUDGE_DEBOUNCE_SECONDS existed, with a comment explaining it, and nothing read it. Providers fire once per document, so twenty files dropped into a watched folder were twenty runs — the in-flight guard answered `busy` to nineteen of them, throwing away nineteen announcements, and the one run that did start had begun before most of the changes landed. A burst now folds into one trailing run, and the binding is re-read when the timer fires so a sync switched off in between does not get a last run out of a stale row. Four defects the new suites turned up, all fixed: the sync button lost its accessible name below sm, where its label is hidden; the stored address was never shown in the connect dialog although the form kept submitting it; the allow-self-signed switch only read the connection at mount, so a stored "on" showed as off and the next save wrote that back; and a refused folder create escaped as an unhandled rejection with nothing said to anybody. Also here: the phone shell gets the same feature (one sheet, four views, the same hook as the desktop), the marks come from the product's own icon files rather than redrawn approximations, and the 22 non-English locales carry the new keys. ADMIN-SVC-069 asserted that `documents` has no MCP surface. It does now, so the test takes its example from MCP_GATED_ADDON_IDS instead of naming one.
332 lines
12 KiB
TypeScript
332 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,
|
|
openDocSyncSignal: 0,
|
|
};
|
|
return { ...base, ...overrides };
|
|
}
|