mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(dayplan): a moving day with a flight draws no drive from one stay to the next
A day that checks out of one hotel and into another, with nothing else planned, falls back to a drive from the first hotel to the second (#1297). A flight, train, ferry or coach booked on that day is the move itself, but one saved without its stations leaves no waypoint behind, so the day map, the trip overview and the phone plan all drew the full road between the two hotels, and the phone showed it above and below the flight (#2476). With its stations the day was already right. - The fallback drive between the two hotels is no longer drawn when any such booking is on the day, located or not. A day without a booking keeps it. No line beats a wrong one. - The phone plan takes the evening hotel leg from the last drive that reaches the hotel, not the first, so an earlier drive to the same spot no longer stands in for the one that closes the day. - Google Maps and CoMaps exports of such a day, which could only ever describe that same drive, return nothing, and on the desktop and the phone the two buttons are left out whenever fewer than two stops remain, instead of doing nothing on a tap. A hotel stop planned by hand ahead of the flight already sorts behind it through the ride seating of #2474; a test now pins that for the days view as well.
This commit is contained in:
@@ -9,7 +9,7 @@ import type { MergedItem } from '../../../../src/utils/dayMerge'
|
||||
import type { Assignment, Day, DayNote, Place, RouteSegment } from '../../../../src/types'
|
||||
import MPlanTimeline from '../../../../src/mobile/screens/trip/plan/MPlanTimeline'
|
||||
|
||||
// FE-MOB-PLTL-001 to FE-MOB-PLTL-047
|
||||
// FE-MOB-PLTL-001 to FE-MOB-PLTL-049
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
tl: {} as Record<string, unknown>,
|
||||
@@ -110,7 +110,9 @@ function buildTl(over: Record<string, unknown> = {}): MPlanTimelineController {
|
||||
addBooking: vi.fn(),
|
||||
addTransport: vi.fn(),
|
||||
optimize: vi.fn(async () => undefined),
|
||||
canExportRoute: true,
|
||||
exportGoogleMaps: vi.fn(),
|
||||
exportCoMaps: vi.fn(),
|
||||
renameDay: vi.fn(),
|
||||
fullPlaceOf: vi.fn(() => undefined as Place | undefined),
|
||||
routeModeOptions: [
|
||||
@@ -583,6 +585,16 @@ describe('MPlanTimeline', () => {
|
||||
expect(mocks.tl.exportGoogleMaps).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-049: a day with no route to hand over offers no Google Maps or CoMaps tile (#2476)', () => {
|
||||
// A moving day whose only content is the flight: the export would be a drive
|
||||
// from one hotel to the other, so the two tiles are left out, not left dead.
|
||||
renderTimeline({ canExportRoute: false }, {}, { mode: 'edit' })
|
||||
|
||||
expect(screen.queryByText('mobileTrip.googleMaps')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('mobileTrip.coMaps')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('dayplan.optimize')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-034: the note tile is inert while no day is selected', () => {
|
||||
const { shell } = renderTimeline({ day: undefined, rows: [], merged: [] }, {}, { mode: 'edit' })
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
type TransportEntry,
|
||||
} from '../../../../src/mobile/screens/trip/plan/planTimelineModel'
|
||||
import { getDisplayTimeForDay, type MergedItem } from '../../../../src/utils/dayMerge'
|
||||
import { buildDayRouteRuns } from '../../../../src/components/Map/dayRoutePlan'
|
||||
import { buildAssignment, buildDayNote, buildPlace, buildReservation } from '../../../helpers/factories'
|
||||
import type {
|
||||
Accommodation, Assignment, Day, DayNote, Reservation, RouteSegment, TranslationFn,
|
||||
} from '../../../../src/types'
|
||||
|
||||
// FE-MOB-PTLM-001 to FE-MOB-PTLM-044
|
||||
// FE-MOB-PTLM-001 to FE-MOB-PTLM-046
|
||||
|
||||
const DAYS = [
|
||||
{ id: 1, trip_id: 1, day_number: 1, date: '2026-05-01', title: null },
|
||||
@@ -309,6 +310,61 @@ describe('planTimelineModel — hotel chips and legs', () => {
|
||||
it('FE-MOB-PTLM-027: returns no legs without an accommodation on the day', () => {
|
||||
expect(hotelLegsForDay(DAY2, DAYS, [], [seg([48.1, 16.1], [48.2, 16.2])])).toEqual({ top: null, bottom: null })
|
||||
})
|
||||
|
||||
it('FE-MOB-PTLM-045: the evening leg is the last drive into the hotel, not the first (#2476)', () => {
|
||||
// A stop planned on the hotel's own spot early in the day: the drive there reaches
|
||||
// the hotel's coordinates first, but the day ends with the drive back from the park.
|
||||
const hotel = accommodation({ id: 7, start_day_id: 1, end_day_id: 3, place_lat: 48.0, place_lng: 16.0 })
|
||||
const out = seg([48.0, 16.0], [48.1, 16.1])
|
||||
const toHotelSpot = seg([48.1, 16.1], [48.0, 16.0])
|
||||
const onward = seg([48.0, 16.0], [48.2, 16.2])
|
||||
const back = seg([48.2, 16.2], [48.0, 16.0])
|
||||
const legs = hotelLegsForDay(DAY2, DAYS, [hotel], [out, toHotelSpot, onward, back])
|
||||
expect(legs.top?.seg).toBe(out)
|
||||
expect(legs.bottom?.seg).toBe(back)
|
||||
})
|
||||
|
||||
describe('a moving day with a flight between the two stays (#2476)', () => {
|
||||
// Day 2 checks out of Munich and into Hamburg; the flight sits between them.
|
||||
const HOTEL_A = { lat: 48.137, lng: 11.575 }
|
||||
const HOTEL_B = { lat: 53.551, lng: 9.993 }
|
||||
const MUC = { lat: 48.353, lng: 11.786 }
|
||||
const HAM = { lat: 53.63, lng: 9.988 }
|
||||
const stays = [
|
||||
accommodation({ id: 1, start_day_id: 1, end_day_id: 2, place_name: 'Hotel A', place_lat: HOTEL_A.lat, place_lng: HOTEL_A.lng }),
|
||||
accommodation({ id: 2, start_day_id: 2, end_day_id: 3, place_name: 'Hotel B', place_lat: HOTEL_B.lat, place_lng: HOTEL_B.lng }),
|
||||
]
|
||||
const flight = (located: boolean) => buildReservation({
|
||||
id: 7, type: 'flight', title: 'LH 2078', day_id: 2, end_day_id: 2,
|
||||
reservation_time: '2026-05-02T15:15', reservation_end_time: '2026-05-02T17:20',
|
||||
endpoints: located
|
||||
? [
|
||||
{ role: 'from', sequence: 0, name: 'MUC', code: null, ...MUC, timezone: null, local_date: null, local_time: null },
|
||||
{ role: 'to', sequence: 1, name: 'HAM', code: null, ...HAM, timezone: null, local_date: null, local_time: null },
|
||||
]
|
||||
: [],
|
||||
})
|
||||
// What the connector calculation hands the timeline: one segment per pair of
|
||||
// neighbouring waypoints in each drawn run.
|
||||
const legsOf = (reservations: Reservation[]) => {
|
||||
const runs = buildDayRouteRuns(2, {
|
||||
days: DAYS, assignments: {}, reservations, accommodations: stays, optimizeFromAccommodation: true,
|
||||
})
|
||||
const segments = runs.flatMap(run => run.slice(1).map((p, i) => seg([run[i].lat, run[i].lng], [p.lat, p.lng])))
|
||||
return hotelLegsForDay(DAY2, DAYS, stays, segments)
|
||||
}
|
||||
|
||||
it('FE-MOB-PTLM-046: shows no drive from one hotel to the other, above or below the flight', () => {
|
||||
// Saved without its airports, the flight leaves nothing to connect: the plan
|
||||
// used to show the whole Munich to Hamburg drive twice, around the flight.
|
||||
expect(legsOf([flight(false)])).toEqual({ top: null, bottom: null })
|
||||
// With them, the morning drive goes to the departure airport and the evening
|
||||
// one comes from the arrival airport.
|
||||
const located = legsOf([flight(true)])
|
||||
expect(located.top).toMatchObject({ name: 'Hotel A', seg: { from: [HOTEL_A.lat, HOTEL_A.lng], to: [MUC.lat, MUC.lng] } })
|
||||
expect(located.bottom).toMatchObject({ name: 'Hotel B', seg: { from: [HAM.lat, HAM.lng], to: [HOTEL_B.lat, HOTEL_B.lng] } })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('planTimelineModel — cityPillsForDay', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { buildPlanner, buildTripActions } from '../../../helpers/mobileTrip'
|
||||
import { resetAllStores, seedStore } from '../../../helpers/store'
|
||||
import { act, renderHook, waitFor } from '../../../helpers/render'
|
||||
|
||||
// FE-MOB-PLTL-001 to FE-MOB-PLTL-045
|
||||
// FE-MOB-PLTL-001 to FE-MOB-PLTL-050
|
||||
|
||||
// The connector calculation is its own hook with real OSRM calls — stubbed here so
|
||||
// the timeline sees exactly the legs a test wants to match against.
|
||||
@@ -604,6 +604,29 @@ describe('useMPlanTimeline', () => {
|
||||
expect(window.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-050: a moving day that only holds its flight has no route to export (#2476)', async () => {
|
||||
// Day 2 checks out of one hotel and into another, the flight in between saved
|
||||
// without its airports. The exports used to open a drive between the two hotels.
|
||||
const stays = [
|
||||
{ ...HOTEL, id: 72, start_day_id: 1, end_day_id: 2, place_name: 'Hotel A' },
|
||||
{ ...HOTEL, id: 73, start_day_id: 2, end_day_id: 3, place_name: 'Hotel B', place_lat: 53.55, place_lng: 9.99 },
|
||||
] as Accommodation[]
|
||||
const flight = buildReservation({ id: 77, type: 'flight', title: 'LH 2078', day_id: 2, end_day_id: 2, reservation_time: '2026-05-02T15:15' })
|
||||
const { result } = await renderTimeline(makePlanner({ assignments: {}, reservations: [flight], tripAccommodations: stays }))
|
||||
expect(result.current.canExportRoute).toBe(false)
|
||||
act(() => { result.current.exportGoogleMaps() })
|
||||
act(() => { result.current.exportCoMaps() })
|
||||
expect(window.open).not.toHaveBeenCalled()
|
||||
|
||||
// The same day without the booking is a drive from one hotel to the other.
|
||||
const byRoad = await renderTimeline(makePlanner({ assignments: {}, reservations: [], tripAccommodations: stays }))
|
||||
expect(byRoad.result.current.canExportRoute).toBe(true)
|
||||
act(() => { byRoad.result.current.exportGoogleMaps() })
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://www.google.com/maps/dir/48,16.05/53.55,9.99', '_blank', 'noopener,noreferrer',
|
||||
)
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-039: renames the day with a trimmed title', async () => {
|
||||
const planner = makePlanner()
|
||||
const { result } = await renderTimeline(planner)
|
||||
|
||||
Reference in New Issue
Block a user