mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(mobile): booking routes on the phone map follow the day they run on
A booking switched on with "On map" stayed drawn on every day of the phone's plan map, so a flight from the first day crossed the map of the third, although a day chip frames one day and drops the other days' pins. The plan map now draws such a booking only on the days it runs on, the arrival day of an overnight one included, and the all-days view still shows every booking that is switched on. The switch itself stays trip-wide, and the desktop, collections and the road trip stage draw exactly as before. "On map" in a booking's sheet now reads as on only where the plan map draws the route. From any other day a tap takes the map to the booking's day, switching the route on when it is off, instead of hiding a route that is not drawn there anyway. The phone settings also gain the "Always show booking routes" switch the desktop already had. As on the desktop it sets the default for trips not yet touched on this device, so a trip where a route was switched on or off by hand keeps that choice.
This commit is contained in:
@@ -599,6 +599,32 @@ describe('MapView', () => {
|
||||
expect(screen.getAllByTestId('polyline').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('FE-COMP-MAPVIEW-082: a booking switched on for another day keeps drawing unless the map is held to the day', () => {
|
||||
// The desktop passes no scope, so a booking switched on stays drawn on every day.
|
||||
// Only a caller that asks for it drops the booking from a day it does not run on.
|
||||
const days = [{ id: 10, trip_id: 1, day_number: 1 }, { id: 11, trip_id: 1, day_number: 2 }]
|
||||
const reservation = buildReservation({
|
||||
id: 44,
|
||||
type: 'flight',
|
||||
day_id: 10,
|
||||
end_day_id: 10,
|
||||
endpoints: [
|
||||
{ role: 'from', sequence: 0, name: 'A', code: 'AAA', lat: 1, lng: 2, timezone: null, local_time: null, local_date: null },
|
||||
{ role: 'to', sequence: 1, name: 'B', code: 'BBB', lat: 3, lng: 4, timezone: null, local_time: null, local_date: null },
|
||||
],
|
||||
})
|
||||
const reservations = [reservation]
|
||||
const ids = [44]
|
||||
const { rerender } = render(<MapView reservations={reservations} visibleConnectionIds={ids} days={days} selectedDayId={11} />)
|
||||
expect(screen.getAllByTestId('polyline').length).toBeGreaterThan(0)
|
||||
|
||||
rerender(<MapView reservations={reservations} visibleConnectionIds={ids} days={days} selectedDayId={11} scopeConnectionsToDay />)
|
||||
expect(screen.queryByTestId('polyline')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<MapView reservations={reservations} visibleConnectionIds={ids} days={days} selectedDayId={10} scopeConnectionsToDay />)
|
||||
expect(screen.getAllByTestId('polyline').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('FE-COMP-MAPVIEW-073: a category icon that throws leaves the marker circle intact', () => {
|
||||
// Icon components come from a lookup table a plugin can extend; one that
|
||||
// throws must not take the whole map down, only its own glyph.
|
||||
|
||||
@@ -699,6 +699,9 @@ export const MapView = memo(function MapView({
|
||||
showTransitRoutes = true,
|
||||
days = [] as Day[],
|
||||
selectedDayId = null,
|
||||
// Whether a booking switched on by hand also has to run on the selected day to be
|
||||
// drawn. Only the phone's plan map asks for it; see RouteVisibilityOptions.
|
||||
scopeConnectionsToDay = false,
|
||||
onReservationClick,
|
||||
pois = [] as Poi[],
|
||||
onPoiClick,
|
||||
@@ -762,8 +765,8 @@ export const MapView = memo(function MapView({
|
||||
</Marker>
|
||||
)), [pois, onPoiClick, onPoiDropOnRoute])
|
||||
const visibleReservations = useMemo(() => (
|
||||
visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes, selectedDayId, days })
|
||||
), [reservations, visibleConnectionIds, showTransitRoutes, selectedDayId, days])
|
||||
visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes, selectedDayId, days, scopeConnectionsToDay })
|
||||
), [reservations, visibleConnectionIds, showTransitRoutes, selectedDayId, days, scopeConnectionsToDay])
|
||||
// Real road geometry for car/bus/taxi/bicycle bookings (straight line until it loads/if it fails).
|
||||
const transportRoutes = useTransportRoutes(visibleReservations)
|
||||
// Dynamic padding: account for sidebars + bottom inspector + day detail panel
|
||||
|
||||
@@ -1379,6 +1379,33 @@ describe('MapViewGL', () => {
|
||||
expect(onReservationClick).toHaveBeenCalledWith(77)
|
||||
})
|
||||
|
||||
it('FE-COMP-MAPVIEWGL-087: a booking switched on for another day reaches the overlay unless the map is held to the day', async () => {
|
||||
loadOnAttach()
|
||||
const days = [{ id: 10, trip_id: 1, day_number: 1 }, { id: 11, trip_id: 1, day_number: 2 }]
|
||||
const flight = {
|
||||
id: 44, trip_id: 1, type: 'flight', title: 'A → B', status: 'confirmed', day_id: 10, end_day_id: 10,
|
||||
endpoints: [
|
||||
{ role: 'from', sequence: 0, name: 'A', code: 'AAA', lat: 1, lng: 2, timezone: null, local_time: null, local_date: null },
|
||||
{ role: 'to', sequence: 1, name: 'B', code: 'BBB', lat: 3, lng: 4, timezone: null, local_time: null, local_date: null },
|
||||
],
|
||||
}
|
||||
const reservations = [flight]
|
||||
const ids = [44]
|
||||
const drawn = () => (reservationOverlay.update.mock.lastCall?.[0] as { id: number }[]).map(r => r.id)
|
||||
|
||||
const { rerender } = render(
|
||||
<MapViewGL places={[]} fitKey={1} reservations={reservations} visibleConnectionIds={ids} days={days} selectedDayId={11} />,
|
||||
)
|
||||
await act(async () => {})
|
||||
expect(drawn()).toEqual([44])
|
||||
|
||||
rerender(
|
||||
<MapViewGL places={[]} fitKey={1} reservations={reservations} visibleConnectionIds={ids} days={days} selectedDayId={11} scopeConnectionsToDay />,
|
||||
)
|
||||
await act(async () => {})
|
||||
expect(drawn()).toEqual([])
|
||||
})
|
||||
|
||||
it('FE-COMP-MAPVIEWGL-042: explore POIs get their own pin, hover popup and click callback', async () => {
|
||||
loadOnAttach()
|
||||
const basePoi: Poi = {
|
||||
|
||||
@@ -164,6 +164,11 @@ interface Props {
|
||||
showTransitRoutes?: boolean
|
||||
days?: Day[]
|
||||
selectedDayId?: number | null
|
||||
/**
|
||||
* Whether a booking switched on by hand also has to run on the selected day to be
|
||||
* drawn. Only the phone's plan map asks for it; see RouteVisibilityOptions.
|
||||
*/
|
||||
scopeConnectionsToDay?: boolean
|
||||
showReservationStats?: boolean
|
||||
onReservationClick?: (reservationId: number) => void
|
||||
pois?: Poi[]
|
||||
@@ -695,6 +700,7 @@ export function MapViewGL({
|
||||
showTransitRoutes = true,
|
||||
days = NO_DAYS,
|
||||
selectedDayId = null,
|
||||
scopeConnectionsToDay = false,
|
||||
showReservationStats = false,
|
||||
onReservationClick,
|
||||
pois = NO_POIS,
|
||||
@@ -2186,8 +2192,8 @@ export function MapViewGL({
|
||||
// DayPlanSidebar — nothing is rendered until the user enables a
|
||||
// booking's route, matching the Leaflet MapView's behaviour.
|
||||
const visibleReservations = useMemo(() => (
|
||||
visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes, selectedDayId, days })
|
||||
), [reservations, visibleConnectionIds, showTransitRoutes, selectedDayId, days])
|
||||
visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes, selectedDayId, days, scopeConnectionsToDay })
|
||||
), [reservations, visibleConnectionIds, showTransitRoutes, selectedDayId, days, scopeConnectionsToDay])
|
||||
// Real road geometry for car/bus/taxi/bicycle bookings (straight line until it loads/if it fails).
|
||||
const transportRoutes = useTransportRoutes(visibleReservations)
|
||||
|
||||
|
||||
@@ -53,6 +53,13 @@ export default function MSettingsGeneral() {
|
||||
on: settings.map_booking_labels === true,
|
||||
value: (on) => on,
|
||||
},
|
||||
{
|
||||
key: 'map_always_show_routes',
|
||||
label: t('settings.alwaysShowRoutes'),
|
||||
sub: t('settings.alwaysShowRoutesHint'),
|
||||
on: settings.map_always_show_routes === true,
|
||||
value: (on) => on,
|
||||
},
|
||||
{
|
||||
key: 'map_poi_pill_enabled',
|
||||
label: t('settings.mapPoiPill'),
|
||||
|
||||
@@ -386,6 +386,10 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
|
||||
// ride's leg carries no geometry, so without this the road ended at the
|
||||
// departure terminal and picked up again at the arrival with nothing between.
|
||||
visibleConnectionIds={onStage ? planner.roadtripConnections : planner.visibleConnections}
|
||||
// The plan map shows one day, so a booking switched on for another day stays off
|
||||
// it until that day is picked, or the whole trip is. The switch itself is still
|
||||
// trip-wide. The stage keeps its seamed rides whatever day they were booked on.
|
||||
scopeConnectionsToDay={!onStage}
|
||||
// Transport overlay tap → the mobile transport detail sheet (desktop
|
||||
// routes this through mapTransportDetail into the day sidebar instead).
|
||||
onReservationClick={(rid: number) => shell.openSheet('transport', { reservationId: rid })}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RES_ICONS } from '../../../../components/Planner/DayPlanSidebar.constan
|
||||
import { splitReservationDateTime } from '../../../../utils/formatters'
|
||||
import { getFlightLegs, getTrainLegs } from '../../../../utils/flightLegs'
|
||||
import { openFile } from '../../../../utils/fileDownload'
|
||||
import { runsOnDay } from '../../../../utils/reservationRoutes'
|
||||
import type { Reservation } from '../../../../types'
|
||||
import { Eyebrow, INNER_CLS, StatBox, TileHeader, displayTime } from './MTripSheetUi'
|
||||
|
||||
@@ -107,7 +108,11 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) {
|
||||
|
||||
const confirmed = res.status === 'confirmed'
|
||||
const codeBlurred = blurCodes && !codeRevealed
|
||||
const onMap = planner.visibleConnections.includes(res.id)
|
||||
// The plan map only draws a switched-on booking on the days it runs on, so from any
|
||||
// other day the button reads off, as the map does, and a tap takes the map to the
|
||||
// booking instead of switching off a route that is not drawn here anyway.
|
||||
const switchedOn = planner.visibleConnections.includes(res.id)
|
||||
const onMap = switchedOn && runsOnDay(res, planner.selectedDayId, planner.days)
|
||||
|
||||
// First tap draws the overlay and jumps to the map; while drawn, the same
|
||||
// button hides it again (per-booking overlay toggle, desktop parity).
|
||||
@@ -116,10 +121,19 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) {
|
||||
planner.toggleConnection(res.id)
|
||||
return
|
||||
}
|
||||
planner.toggleConnection(res.id)
|
||||
if (!switchedOn) planner.toggleConnection(res.id)
|
||||
shell.closeSheet()
|
||||
if (shell.trTab !== 'plan') shell.setTrTab('plan')
|
||||
if (shell.view !== 'map') shell.toggleView()
|
||||
// The map draws the day on the chips, and the Transports tab lists the whole trip, so
|
||||
// the booking may run on a different day. Moved last, because entering the map picks
|
||||
// the day the shell was already holding. A day inside the booking's span, the
|
||||
// all-days view and a booking bound to no day are left as they are.
|
||||
const bookingDayId = res.day_id ?? res.end_day_id
|
||||
if (planner.selectedDayId != null && bookingDayId != null && !runsOnDay(res, planner.selectedDayId, planner.days)) {
|
||||
planner.handleSelectDay(bookingDayId, false)
|
||||
planner.setExpandedDayIds(new Set([bookingDayId]))
|
||||
}
|
||||
}
|
||||
|
||||
const editTransport = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { hopIsVisible, isRoutableReservation, labelFloorPx, lineFloorPx, visibleRouteReservations } from './reservationRoutes'
|
||||
import { hopIsVisible, isRoutableReservation, labelFloorPx, lineFloorPx, runsOnDay, visibleRouteReservations } from './reservationRoutes'
|
||||
import type { Day, Reservation, ReservationEndpoint } from '../types'
|
||||
|
||||
function endpoint(role: 'from' | 'to', lat: number, lng: number): ReservationEndpoint {
|
||||
@@ -117,6 +117,92 @@ describe('visibleRouteReservations with a selected day', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// The phone's plan map shows one day, so it asks for the per-item toggle to follow the
|
||||
// selected day the way the transit toggle does. The span rule is the transit one.
|
||||
describe('visibleRouteReservations with the per-item toggle scoped to the day', () => {
|
||||
const twoStop = [endpoint('from', 1, 2), endpoint('to', 3, 4)]
|
||||
const days = [
|
||||
{ id: 10, trip_id: 1, day_number: 1 },
|
||||
{ id: 11, trip_id: 1, day_number: 2 },
|
||||
{ id: 20, trip_id: 1, day_number: 3 },
|
||||
] as Day[]
|
||||
|
||||
function booking(overrides: Partial<Reservation>): Reservation {
|
||||
return reservation({ id: 5, type: 'flight', endpoints: twoStop, ...overrides })
|
||||
}
|
||||
|
||||
const drawn = (r: Reservation, selectedDayId: number | null | undefined, onDays: Day[] = days) =>
|
||||
visibleRouteReservations([r], {
|
||||
visibleConnectionIds: [r.id], showTransitRoutes: false, selectedDayId, days: onDays, scopeConnectionsToDay: true,
|
||||
})
|
||||
|
||||
it('leaves out a booking switched on for another day', () => {
|
||||
expect(drawn(booking({ day_id: 20, end_day_id: 20 }), 10)).toEqual([])
|
||||
})
|
||||
|
||||
it('draws a booking on the day it runs on', () => {
|
||||
const r = booking({ day_id: 10, end_day_id: 10 })
|
||||
expect(drawn(r, 10)).toEqual([r])
|
||||
})
|
||||
|
||||
it('keeps an overnight booking on both of its days and off the rest', () => {
|
||||
const r = booking({ day_id: 10, end_day_id: 11 })
|
||||
expect(drawn(r, 10)).toEqual([r])
|
||||
expect(drawn(r, 11)).toEqual([r])
|
||||
expect(drawn(r, 20)).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps a multi-day booking on the days in between', () => {
|
||||
const r = booking({ day_id: 10, end_day_id: 20 })
|
||||
expect(drawn(r, 11)).toEqual([r])
|
||||
})
|
||||
|
||||
it('goes by day order, not by day id', () => {
|
||||
const reordered = [
|
||||
{ id: 20, trip_id: 1, day_number: 1 },
|
||||
{ id: 11, trip_id: 1, day_number: 2 },
|
||||
{ id: 10, trip_id: 1, day_number: 3 },
|
||||
] as Day[]
|
||||
const r = booking({ day_id: 20, end_day_id: 11 })
|
||||
expect(drawn(r, 11, reordered)).toEqual([r])
|
||||
expect(drawn(r, 10, reordered)).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps a booking that is bound to no day', () => {
|
||||
const r = booking({ day_id: null, end_day_id: null })
|
||||
expect(drawn(r, 10)).toEqual([r])
|
||||
})
|
||||
|
||||
it('keeps every switched-on booking while no day is selected', () => {
|
||||
const r = booking({ day_id: 20, end_day_id: 20 })
|
||||
expect(drawn(r, undefined)).toEqual([r])
|
||||
expect(drawn(r, null)).toEqual([r])
|
||||
})
|
||||
|
||||
it('still draws nothing that was not switched on', () => {
|
||||
const r = booking({ day_id: 10, end_day_id: 10 })
|
||||
expect(visibleRouteReservations([r], {
|
||||
visibleConnectionIds: [], showTransitRoutes: false, selectedDayId: 10, days, scopeConnectionsToDay: true,
|
||||
})).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('runsOnDay', () => {
|
||||
const days = [
|
||||
{ id: 10, trip_id: 1, day_number: 1 },
|
||||
{ id: 11, trip_id: 1, day_number: 2 },
|
||||
] as Day[]
|
||||
|
||||
it('reads a booking with only an arrival day as running on that day', () => {
|
||||
expect(runsOnDay({ day_id: null, end_day_id: 11 }, 11, days)).toBe(true)
|
||||
expect(runsOnDay({ day_id: null, end_day_id: 11 }, 10, days)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not rule a booking out for a selected day it cannot find', () => {
|
||||
expect(runsOnDay({ day_id: 10, end_day_id: 10 }, 99, days)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hopIsVisible (#2275)', () => {
|
||||
// A flat projection: one unit of lat or lng is one pixel.
|
||||
const px = (p: readonly [number, number]) => ({ x: p[1], y: p[0] })
|
||||
|
||||
@@ -22,10 +22,17 @@ export interface RouteVisibilityOptions {
|
||||
selectedDayId?: number | null
|
||||
/** The trip's days, needed to order them — day ids are not monotonic once days have been reordered. */
|
||||
days?: Day[]
|
||||
/**
|
||||
* Hold the per-item toggle to the selected day as well. Off unless asked for: the desktop
|
||||
* keeps a booking it was told to draw on every day, since switching it on is a deliberate
|
||||
* act (#2019). The phone's plan map shows one day at a time, and a flight from another
|
||||
* day drawn across it reads as part of that day.
|
||||
*/
|
||||
scopeConnectionsToDay?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this transit journey actually run on the selected day? `showTransitRoutes` is one
|
||||
* Does this journey actually run on the selected day? `showTransitRoutes` is one
|
||||
* day's toggle but the reservation list is trip-wide, so without this every automated
|
||||
* transport in the trip drew its geometry as soon as any day's route was switched on (#2019).
|
||||
*
|
||||
@@ -34,7 +41,11 @@ export interface RouteVisibilityOptions {
|
||||
* A journey bound to no day keeps drawing; hiding something that legitimately belongs
|
||||
* to today is the worse failure.
|
||||
*/
|
||||
function transitRunsOnDay(r: Reservation, selectedDayId: number | null | undefined, days: Day[]): boolean {
|
||||
export function runsOnDay(
|
||||
r: Pick<Reservation, 'day_id' | 'end_day_id'>,
|
||||
selectedDayId: number | null | undefined,
|
||||
days: Day[],
|
||||
): boolean {
|
||||
if (selectedDayId == null) return true
|
||||
const startDayId = r.day_id ?? r.end_day_id
|
||||
if (startDayId == null) return true
|
||||
@@ -45,11 +56,11 @@ function transitRunsOnDay(r: Reservation, selectedDayId: number | null | undefin
|
||||
|
||||
/** Which reservations should draw a route on the map, combining the two independent toggles above. */
|
||||
export function visibleRouteReservations(reservations: Reservation[], options: RouteVisibilityOptions): Reservation[] {
|
||||
const { visibleConnectionIds, showTransitRoutes, selectedDayId, days } = options
|
||||
const { visibleConnectionIds, showTransitRoutes, selectedDayId, days, scopeConnectionsToDay } = options
|
||||
const set = new Set(visibleConnectionIds || [])
|
||||
return reservations.filter(r =>
|
||||
(r.type === 'transit' && showTransitRoutes && transitRunsOnDay(r, selectedDayId, days || [])) ||
|
||||
set.has(r.id)
|
||||
(r.type === 'transit' && showTransitRoutes && runsOnDay(r, selectedDayId, days || [])) ||
|
||||
(set.has(r.id) && (!scopeConnectionsToDay || runsOnDay(r, selectedDayId, days || [])))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -190,4 +190,31 @@ describe('MSettingsGeneral', () => {
|
||||
|
||||
expect(updateSetting).toHaveBeenCalledWith('start_trip_tab', 'finanzplan');
|
||||
});
|
||||
|
||||
// #2456: the account-wide default behind every trip's booking routes. Desktop has it
|
||||
// under Settings -> Display; a phone had no way to reach it, so a booking route on the
|
||||
// phone map only ever came from switching each booking on by hand.
|
||||
it('FE-MOB-SET-015: the travel card carries the always-show-booking-routes default, off until set', async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }), updateSetting });
|
||||
render(<MSettingsGeneral />);
|
||||
|
||||
const toggle = screen.getByRole('switch', { name: 'Always show booking routes' });
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
await user.click(toggle);
|
||||
expect(updateSetting).toHaveBeenCalledWith('map_always_show_routes', true);
|
||||
});
|
||||
|
||||
it('FE-MOB-SET-016: the always-show-booking-routes toggle reflects a stored true and switches it off', async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', map_always_show_routes: true }), updateSetting });
|
||||
render(<MSettingsGeneral />);
|
||||
|
||||
const toggle = screen.getByRole('switch', { name: 'Always show booking routes' });
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
await user.click(toggle);
|
||||
expect(updateSetting).toHaveBeenCalledWith('map_always_show_routes', false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,13 @@ import { MAP_LAYER_SWITCHER_INSET, MAP_ROUND_CONTROL_SIZE } from '../../../../sr
|
||||
import { useSettingsStore } from '../../../../src/store/settingsStore'
|
||||
import { useTripStore } from '../../../../src/store/tripStore'
|
||||
import { seedStore } from '../../../helpers/store'
|
||||
import type { Place } from '../../../../src/types'
|
||||
import type { Day, Place, Reservation } from '../../../../src/types'
|
||||
import { visibleRouteReservations, type RouteVisibilityOptions } from '../../../../src/utils/reservationRoutes'
|
||||
import type { AlternativeOverlay } from '../../../../src/components/Roadtrip/alternativeOverlays'
|
||||
import type { LegAlternatives } from '../../../../src/components/Roadtrip/useRouteAlternatives'
|
||||
import { RT_ALT_BAR_LIFT } from '../../../../src/mobile/screens/trip/roadtrip/useMRtAlternatives'
|
||||
|
||||
// FE-MOB-MAPAREA-001 to FE-MOB-MAPAREA-037
|
||||
// FE-MOB-MAPAREA-001 to FE-MOB-MAPAREA-042
|
||||
//
|
||||
// The stage's pins come out of the trip store rather than the planner's map list, so the
|
||||
// stage fixtures seed the store and leave `mapPlaces` to stand for what the plan tab shows.
|
||||
@@ -791,10 +792,99 @@ describe('MMapArea', () => {
|
||||
const { rerender } = render(<MMapArea planner={planner} shell={shell} />)
|
||||
|
||||
expect(mocks.props.visibleConnectionIds).toEqual([70])
|
||||
// A seamed ride joins the road whatever day it was booked on, so the stage does not
|
||||
// hold bookings to the selected day the way the plan map does.
|
||||
expect(mocks.props.scopeConnectionsToDay).toBe(false)
|
||||
|
||||
// The plan tab keeps the traveller's own toggles, as the desktop does off road trip mode.
|
||||
rerender(<MMapArea planner={planner} shell={{ ...shell, trTab: 'plan' }} />)
|
||||
expect(mocks.props.visibleConnectionIds).toBe(planner.visibleConnections)
|
||||
expect(mocks.props.visibleConnectionIds).toEqual([])
|
||||
expect(mocks.props.scopeConnectionsToDay).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* #2456: the phone map is one day's picture. A day chip frames the day, draws its route
|
||||
* and drops the other days' pins (MTripShell.selectDayOnMap), and the automated
|
||||
* transports ride the day since #2019. A booking switched on through its own toggle
|
||||
* ("On map") stayed on every day's map, though, because the per-booking branch of
|
||||
* visibleRouteReservations is trip-wide.
|
||||
*
|
||||
* The renderer is mocked here, so what it WOULD draw is worked out the way both engines
|
||||
* work it out: MapView and MapViewGL hand their own props to visibleRouteReservations.
|
||||
* The whole prop bag goes in, so a day-scoping option added to the renderers under the
|
||||
* util's own name is picked up as well as a filtered id list is.
|
||||
*/
|
||||
describe('MMapArea booking routes on the day-scoped plan map (#2456)', () => {
|
||||
const DAYS = [
|
||||
{ id: 10, trip_id: 1, day_number: 1, date: '2026-05-01' },
|
||||
{ id: 11, trip_id: 1, day_number: 2, date: '2026-05-02' },
|
||||
{ id: 12, trip_id: 1, day_number: 3, date: '2026-05-03' },
|
||||
] as Day[]
|
||||
|
||||
function flight(over: Partial<Reservation> = {}): Reservation {
|
||||
return {
|
||||
id: 7, trip_id: 1, type: 'flight', title: 'FRA → LIS', status: 'confirmed',
|
||||
day_id: 10, end_day_id: 10,
|
||||
reservation_time: '2026-05-01T09:00', reservation_end_time: '2026-05-01T11:00',
|
||||
endpoints: [
|
||||
{ role: 'from', sequence: 0, name: 'Frankfurt', code: 'FRA', lat: 50.03, lng: 8.57, timezone: null, local_time: null, local_date: null },
|
||||
{ role: 'to', sequence: 1, name: 'Lisbon', code: 'LIS', lat: 38.77, lng: -9.13, timezone: null, local_time: null, local_date: null },
|
||||
],
|
||||
...over,
|
||||
} as unknown as Reservation
|
||||
}
|
||||
|
||||
/** The plan tab's map in front, a booking switched on, and the given day on the chips. */
|
||||
function renderPlanMap(selectedDayId: number | null, booking: Reservation = flight()) {
|
||||
const planner = buildPlanner({
|
||||
days: DAYS,
|
||||
selectedDayId,
|
||||
reservations: [booking],
|
||||
visibleConnections: [booking.id],
|
||||
transitRoutesShown: false,
|
||||
} as unknown as Partial<TripPlanner>)
|
||||
render(<MMapArea planner={planner} shell={buildShell({ view: 'map', mapFront: true, trTab: 'plan' })} />)
|
||||
}
|
||||
|
||||
/** The booking ids either renderer would draw a route for, from the props it was handed. */
|
||||
const drawnBookingIds = () => visibleRouteReservations(
|
||||
(mocks.props.reservations as Reservation[]) ?? [],
|
||||
mocks.props as unknown as RouteVisibilityOptions,
|
||||
).map(r => r.id)
|
||||
|
||||
it('FE-MOB-MAPAREA-038: a booking switched on for day 1 is not drawn on the map of day 3', () => {
|
||||
renderPlanMap(12)
|
||||
|
||||
expect(drawnBookingIds()).toEqual([])
|
||||
})
|
||||
|
||||
it('FE-MOB-MAPAREA-039: the booking stays drawn on the day it runs on', () => {
|
||||
renderPlanMap(10)
|
||||
|
||||
expect(drawnBookingIds()).toEqual([7])
|
||||
})
|
||||
|
||||
it('FE-MOB-MAPAREA-040: an overnight booking is drawn on both of its days and on no other', () => {
|
||||
const overnight = flight({ day_id: 10, end_day_id: 11 })
|
||||
|
||||
renderPlanMap(11, overnight)
|
||||
expect(drawnBookingIds()).toEqual([7])
|
||||
|
||||
renderPlanMap(12, overnight)
|
||||
expect(drawnBookingIds()).toEqual([])
|
||||
})
|
||||
|
||||
it('FE-MOB-MAPAREA-041: the all days view keeps every switched-on booking', () => {
|
||||
renderPlanMap(null)
|
||||
|
||||
expect(drawnBookingIds()).toEqual([7])
|
||||
})
|
||||
|
||||
it('FE-MOB-MAPAREA-042: a booking bound to no day keeps drawing on every day', () => {
|
||||
renderPlanMap(12, flight({ day_id: null, end_day_id: null }))
|
||||
|
||||
expect(drawnBookingIds()).toEqual([7])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
|
||||
import { resetAllStores, seedStore } from '../../../helpers/store'
|
||||
import { fireEvent, render, screen } from '../../../helpers/render'
|
||||
|
||||
// FE-MOB-TRSH-001 to FE-MOB-TRSH-024
|
||||
// FE-MOB-TRSH-001 to FE-MOB-TRSH-030
|
||||
|
||||
vi.mock('../../../../src/utils/fileDownload', () => ({ openFile: vi.fn() }))
|
||||
|
||||
@@ -301,4 +301,110 @@ describe('MTransportSheet', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
expect(shell.closeSheet).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
/**
|
||||
* #2456: the phone map is one day's picture, so "On map" has to land on a day the
|
||||
* booking runs on. The Transports tab lists the whole trip, and the day the chips hold
|
||||
* can be any other day: switched on from there, the route went to a map that frames,
|
||||
* and (once booking routes follow the day) draws, a different day.
|
||||
*
|
||||
* The booking's day goes in after the view switch: entering the map re-selects the day
|
||||
* the shell still holds (MTripShell.toggleView), so it has to come last to win.
|
||||
*/
|
||||
describe('On map lands on a day the booking runs on (#2456)', () => {
|
||||
const DAYS = [
|
||||
{ id: 3, trip_id: 1, day_number: 1, date: '2026-05-01' },
|
||||
{ id: 4, trip_id: 1, day_number: 2, date: '2026-05-02' },
|
||||
{ id: 5, trip_id: 1, day_number: 3, date: '2026-05-03' },
|
||||
]
|
||||
|
||||
it("FE-MOB-TRSH-025: from another day, switches the map to the booking's own day", () => {
|
||||
const planner = makePlanner({ days: DAYS, selectedDayId: 5 })
|
||||
const shell = makeShell({ trTab: 'transports', view: 'plan' })
|
||||
render(<MTransportSheet planner={planner} shell={shell} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'On map' }))
|
||||
|
||||
expect(planner.toggleConnection).toHaveBeenCalledWith(7)
|
||||
expect(planner.handleSelectDay).toHaveBeenCalledWith(3, false)
|
||||
expect(planner.setExpandedDayIds).toHaveBeenCalledWith(new Set([3]))
|
||||
const viewSwitch = vi.mocked(shell.toggleView).mock.invocationCallOrder[0]
|
||||
const daySwitches = vi.mocked(planner.handleSelectDay).mock.invocationCallOrder
|
||||
expect(daySwitches[daySwitches.length - 1]).toBeGreaterThan(viewSwitch)
|
||||
})
|
||||
|
||||
it("FE-MOB-TRSH-026: with the map already in front, still moves it to the booking's day", () => {
|
||||
const planner = makePlanner({ days: DAYS, selectedDayId: 5 })
|
||||
const shell = makeShell({ trTab: 'transports', view: 'map' })
|
||||
render(<MTransportSheet planner={planner} shell={shell} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'On map' }))
|
||||
|
||||
expect(shell.toggleView).not.toHaveBeenCalled()
|
||||
expect(planner.handleSelectDay).toHaveBeenCalledWith(3, false)
|
||||
expect(planner.setExpandedDayIds).toHaveBeenCalledWith(new Set([3]))
|
||||
})
|
||||
|
||||
it("FE-MOB-TRSH-027: keeps the selected day while it is one of the booking's days", () => {
|
||||
// Overnight: departs on day 3, lands on day 4, which the chips hold.
|
||||
const overnight = { ...FLIGHT, end_day_id: 4 } as unknown as Reservation
|
||||
const planner = makePlanner({ days: DAYS, selectedDayId: 4, reservations: [overnight] })
|
||||
render(<MTransportSheet planner={planner} shell={makeShell({ trTab: 'plan', view: 'plan' })} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'On map' }))
|
||||
|
||||
expect(planner.toggleConnection).toHaveBeenCalledWith(7)
|
||||
expect(planner.handleSelectDay).not.toHaveBeenCalled()
|
||||
expect(planner.setExpandedDayIds).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-MOB-TRSH-028: leaves the all days view and a booking bound to no day alone', () => {
|
||||
const unbound = { ...FLIGHT, day_id: null, end_day_id: null } as unknown as Reservation
|
||||
const allDays = makePlanner({ days: DAYS, selectedDayId: null })
|
||||
const { unmount } = render(<MTransportSheet planner={allDays} shell={makeShell({ trTab: 'transports', view: 'map' })} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'On map' }))
|
||||
expect(allDays.handleSelectDay).not.toHaveBeenCalled()
|
||||
unmount()
|
||||
|
||||
const noDay = makePlanner({ days: DAYS, selectedDayId: 5, reservations: [unbound] })
|
||||
render(<MTransportSheet planner={noDay} shell={makeShell({ trTab: 'transports', view: 'plan' })} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'On map' }))
|
||||
expect(noDay.toggleConnection).toHaveBeenCalledWith(7)
|
||||
expect(noDay.handleSelectDay).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// A booking already switched on is still left off another day's map, which is also
|
||||
// where "Always show booking routes" leaves every booking. The button follows the map
|
||||
// there, and a tap takes it to the booking rather than switching the route off unseen.
|
||||
it('FE-MOB-TRSH-029: a booking switched on but not drawn on this day reads off and a tap moves to its day', () => {
|
||||
const planner = makePlanner({ days: DAYS, selectedDayId: 5, visibleConnections: [7] })
|
||||
const shell = makeShell({ trTab: 'transports', view: 'plan' })
|
||||
render(<MTransportSheet planner={planner} shell={shell} />)
|
||||
|
||||
const btn = screen.getByRole('button', { name: 'On map' })
|
||||
expect(btn).toHaveAttribute('aria-pressed', 'false')
|
||||
fireEvent.click(btn)
|
||||
|
||||
expect(planner.toggleConnection).not.toHaveBeenCalled()
|
||||
expect(shell.closeSheet).toHaveBeenCalledTimes(1)
|
||||
expect(shell.setTrTab).toHaveBeenCalledWith('plan')
|
||||
expect(planner.handleSelectDay).toHaveBeenCalledWith(3, false)
|
||||
expect(planner.setExpandedDayIds).toHaveBeenCalledWith(new Set([3]))
|
||||
})
|
||||
|
||||
it("FE-MOB-TRSH-030: on one of the booking's days a switched-on route reads on and a tap hides it in place", () => {
|
||||
const overnight = { ...FLIGHT, end_day_id: 4 } as unknown as Reservation
|
||||
const planner = makePlanner({ days: DAYS, selectedDayId: 4, reservations: [overnight], visibleConnections: [7] })
|
||||
const shell = makeShell({ trTab: 'plan', view: 'map' })
|
||||
render(<MTransportSheet planner={planner} shell={shell} />)
|
||||
|
||||
const btn = screen.getByRole('button', { name: 'On map' })
|
||||
expect(btn).toHaveAttribute('aria-pressed', 'true')
|
||||
fireEvent.click(btn)
|
||||
|
||||
expect(planner.toggleConnection).toHaveBeenCalledWith(7)
|
||||
expect(shell.closeSheet).not.toHaveBeenCalled()
|
||||
expect(planner.handleSelectDay).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,6 +93,8 @@ Flights, trains, cars, and cruises can be drawn as overlays between their endpoi
|
||||
- **Show all / hide all** — a route icon button in the day-plan toolbar (next to Undo/Reorder) flips the whole trip between showing every routable booking and showing none. It is a clean slate rather than a layer on top: whatever you had set with the per-booking **Route** icons is discarded, so pressing it twice leaves you with all routes on or all off, not back where you started. (Automated public-transit journeys have a second gate of their own — see the transit bullet.)
|
||||
- **Always show booking routes** (Settings → General → Travel & map) — an account-wide default that shows every booking's route automatically on any trip you haven't touched before. It sets the *default* only — a trip where you've already used the per-booking toggle or the bulk button keeps its own choice even if you change this setting afterwards.
|
||||
|
||||
On a phone the plan map shows one day at a time, so a booking you have switched on is drawn there only on the days it runs on, the arrival day of an overnight journey included, and in the all-days view. The **On map** button in a booking's detail sheet reads as on only where the route is drawn; from any other day it takes the map to the booking's own day instead of hiding the route. The desktop map and the road trip stage keep drawing a switched-on booking on every day.
|
||||
|
||||
> **Tip:** Whether endpoint text labels appear on the endpoint markers is your own choice — the **Booking route labels** setting in Settings → General → Travel & map (`map_booking_labels`). It is off by default; with it off, the endpoint markers show only the transport icon.
|
||||
|
||||
## Plugin map markers
|
||||
|
||||
Reference in New Issue
Block a user