diff --git a/client/src/mobile/screens/trip/map/MMapArea.tsx b/client/src/mobile/screens/trip/map/MMapArea.tsx
index e4a5788b7..c3e6d4e9e 100644
--- a/client/src/mobile/screens/trip/map/MMapArea.tsx
+++ b/client/src/mobile/screens/trip/map/MMapArea.tsx
@@ -311,27 +311,28 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
// timeline (z-10) or the browse/tab overlays (z-30) above it.
//
// --m-map-floor is the top edge of whatever the map ends at: the dock, 62px tall at
- // safe-bottom + 12, or on the road trip tab the stage bar, which takes the band the
- // round controls would otherwise sit in and is what --m-stage-lift adds. The round
- // controls sit straight on that floor and add their own 12px, close enough to the
- // thumb to reach one-handed. Everything that reads --bottom-nav-h (the compass, both
- // engines' locate button and base-layer switcher, the overview stack) follows on its
- // own. The band used to float a further 38px up to leave the corner under it to the
- // map credit; the phone map carries no visible credit any more (see mobile.css), so
- // that row would now only be a gap over the dock.
+ // safe-bottom + 12, or the alternatives bar when the picker puts one over it, which is
+ // what --m-stage-lift adds. The round controls sit straight on that floor and add
+ // their own 12px, close enough to the thumb to reach one-handed. Everything that reads
+ // --bottom-nav-h (the compass, both engines' locate button and base-layer switcher,
+ // the overview stack) follows on its own. The band used to float a further 38px up to
+ // leave the corner under it to the map credit; the phone map carries no visible credit
+ // any more (see mobile.css), so that row would now only be a gap over the dock.
//
// The metrics are classes rather than an inline --bottom-nav-h, and the lift is its
// OWN variable folded into them: the compass band below is identified by being the
// one element that sets that name inline, and a second one would make that ambiguous.
diff --git a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
index d7f3aa5c0..1e9854a0d 100644
--- a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
+++ b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
@@ -7,14 +7,10 @@ import { useMRtAlternatives } from './useMRtAlternatives'
import MRtCorridorBar from './MRtCorridorBar'
import MRtAlternativesBar from './MRtAlternativesBar'
import { RtAutoRow, RtDryRow, RtLegRow, RtSpillRow, RtStopRow, type RowChrome } from './MRoadtripRows'
-import { badgeLabel, distanceBadge } from './stageBadges'
import MBadge from '../../../components/MBadge'
import MDancingTrek from '../../../components/MDancingTrek'
-import PlaceAvatar from '../../../../components/shared/PlaceAvatar'
-import { dayColor } from '../../../../components/Roadtrip/dayColors'
import { formatDurationShort } from '../../../../components/Roadtrip/roadtripModel'
import { getNavigationTargets } from '../../../../components/Planner/placeNavigation'
-import { useRoadtripSettings } from '../../../../hooks/useRoadtripSettings'
import { useSettingsStore } from '../../../../store/settingsStore'
import { formatDistance } from '../../../../utils/units'
import { isRtlLanguage } from '../../../../i18n'
@@ -84,29 +80,32 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
)
- // ── Map half: the two bars, and nothing else. Everything between them belongs to the
- // map instance the shell keeps mounted, so this layer must not swallow taps.
+ // ── Map half: the search bar, and the picker's bar while it is open. Everything
+ // between them belongs to the map instance the shell keeps mounted, so this layer must
+ // not swallow taps.
//
- // While other ways of driving a leg are on offer, their bar takes the stage bar's slot
- // and the search bar steps away. The picker is modal on the map: its lines are the
+ // A stage bar used to stand over the dock whenever the map was up: the day's
+ // destination with its picture, its stop count, its arrival and its distance. It went
+ // because the day and the distance are already in the shell's stage header, and on a
+ // map that fills the screen a permanent card over the bottom edge costs more than the
+ // three facts it added.
+ //
+ // While other ways of driving a leg are on offer, their bar stands in that now empty
+ // slot and the search bar steps away. The picker is modal on the map: its lines are the
// question on screen, a corridor search started under it would draw its pins over
// them, and the band it frees at the top is room the leg is framed into.
if (shell.rtView === 'map') {
return (
{!alts.open && searchBar}
- {/* Just above the dock, the same gap everything else on this shell keeps from
- it. Twenty pixels higher left a band of map between the two that read as a
- gap rather than as breathing room, and pushed the map's own buttons into
- the bar's top edge. */}
-
- {/* `rt` is handed down rather than looked up again: useMRoadtrip owns a
- 30s interval and a network subscription, and a second call would run a
- second pair of them for the same screen. */}
- {alts.open
- ?
- : }
-
+ {/* Just above the dock, the same gap everything else on this shell keeps from it.
+ Mounted only while the picker is open, so nothing invisible lies over the map's
+ own buttons the rest of the time. */}
+ {alts.open && (
+
+
+
+ )}
)
}
@@ -256,107 +255,6 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
)
}
-/**
- * The stage bar over the map: which day, where to, what it costs.
- *
- * One line rather than a sheet that can be dragged open. A drag sheet brings a panel
- * height the map fit has to account for and lifts the locate and style buttons with
- * it, which is a bigger change than this half of the screen needs to be useful.
- *
- * It reads as a place, so a tap opens that place's stop, the same sheet its row in the
- * chain opens. It used to switch to the chain instead, which broke the promise its name
- * makes; the header's list switch is the way back to the chain. Name, picture, clock and
- * tap all come off one row (see stageEnd), so the bar can never name one stop and open
- * another.
- *
- * It leads with a picture of that place rather than a coloured dot, because a place is
- * recognised by how it looks long before its name is read. The facts beside it are badges
- * rather than one line joined with dots: each one is a figure on its own, and a pill edge
- * separates them at a glance where a middle dot has to be found first. Every badge is
- * neutral. The whole bar is one button, and a filled pill inside it would read as a
- * second control that does something else.
- *
- * Its height is a contract: 38px picture, a 39px text column, 10px padding and the border
- * make the 61px the map area lifts its round buttons over (--m-stage-lift in MMapArea).
- * Growing the picture or a badge moves those buttons into the bar's top edge.
- */
-function StageBar({ planner, rt, onOpen }: {
- planner: MTripTabPanelProps['planner']
- rt: ReturnType
- onOpen: (row: StopRow) => void
-}) {
- const { t } = planner
- const unit = useSettingsStore(s => s.settings.distance_unit)
- const dayColorsOn = useRoadtripSettings(s => s.roadtrip_day_colors, planner.tripId)
- const stage = rt.stage
-
- if (!stage) {
- // No stage means the day filter is off, which on the map is the whole drive. Its total
- // is left out while nothing is measured, rather than printed as a drive of 0 m.
- const total = distanceBadge(planner.roadtripRoutes.totalDistance, unit)
- return (
-
- {t('roadtrip.title')}
- {total && {total}}
-
- )
- }
-
- const tint = dayColorsOn ? dayColor(stage.dayNumber) : null
- const end = rt.end
- const name = end?.stop.name ?? t('roadtrip.stop.none')
- const stopCount = t('roadtrip.day.stopCount', { count: rt.stops })
- const distance = distanceBadge(stage.distance, unit)
- const pending = t('roadtrip.leg.pending')
-
- const inner = (
- <>
- {/* Hidden from the name, which the title already carries: the photo's alt text is the
- place name a second time. The day colour rings the picture when the day colours
- are on, because the ring is what ties this bar to its own line on the map. */}
-
- {/* Keyed by place: the avatar takes its photo from the first place it is handed and
- keeps it while a new one is still loading or has none, so without a remount a
- swipe to the next day would still picture the previous day's destination. */}
- {rt.endPlace ? (
-
- ) : (
-
- )}
-
-
- {name}
-
- {stopCount}
- {/* dir=ltr for the reason the rows carry it: a clock reads the same way round in an RTL locale. */}
- {end?.time && {end.time}}
-
-
- {distance ? {distance} : {pending}}
- >
- )
- const box = 'flex w-full items-center gap-2.5 rounded-[22px] border border-[color:var(--m-cbr)] bg-[color:var(--m-card)] px-[11px] py-[10px] text-left shadow-[0_16px_44px_-14px_rgba(0,0,0,.35)] backdrop-blur-[24px] backdrop-saturate-[1.6]'
-
- // A stage without a single stop has nothing for a tap to open, so a plain block rather
- // than a dead button, the rule the whole-drive line above follows too.
- if (!end) return
{inner}
- return (
-
- )
-}
-
/**
* What is coming up, measured against the clock rather than the plan.
*
diff --git a/client/src/mobile/screens/trip/roadtrip/useMRtAlternatives.ts b/client/src/mobile/screens/trip/roadtrip/useMRtAlternatives.ts
index bbb3eb5df..5a990b187 100644
--- a/client/src/mobile/screens/trip/roadtrip/useMRtAlternatives.ts
+++ b/client/src/mobile/screens/trip/roadtrip/useMRtAlternatives.ts
@@ -12,10 +12,10 @@ export const RT_ALT_BAR_HEIGHT = 174
/**
* How far the map floor rises while the alternatives bar stands over the dock, in pixels.
*
- * The bar's height plus the same 15px the 76px stage lift adds to the 61px stage bar: the
- * gap between the top of the dock and the foot of a bar standing over it. So the floor
- * lands on this bar's top edge as it lands on that one's, and the credit and the round
- * controls clear this bar the way they clear that one.
+ * The bar's height plus the 15px gap between the top of the dock and the foot of a bar
+ * standing over it. So the floor lands on this bar's top edge and the round controls
+ * clear it rather than sitting in it. It is the only bar left in that slot: the stage bar
+ * that used to hold it whenever the map was up is gone.
*
* A number rather than a CSS length because the map's fit padding is added up from it.
* Kept in this module rather than in the bar's, so the map area reads it without pulling
diff --git a/client/tests/unit/mobile/trip/MMapArea.test.tsx b/client/tests/unit/mobile/trip/MMapArea.test.tsx
index 8814ed170..7abe5b0d2 100644
--- a/client/tests/unit/mobile/trip/MMapArea.test.tsx
+++ b/client/tests/unit/mobile/trip/MMapArea.test.tsx
@@ -202,26 +202,29 @@ describe('MMapArea', () => {
expect(compassBand(container)?.className).not.toContain('left-3')
})
- it('FE-MOB-MAPAREA-003: the map layer floats those controls one credit row above the dock', () => {
+ it('FE-MOB-MAPAREA-003: the map layer stands those controls on the dock, with no credit row under them', () => {
const { container } = renderArea()
const layer = container.firstElementChild as HTMLElement
// The floor is the dock's top edge: 62px tall at safe-bottom + 12, raised by the stage lift.
expect(layer.className)
.toContain('[--m-map-floor:calc(env(safe-area-inset-bottom,0px)+74px+var(--m-stage-lift,0px))]')
- // The band sits one credit row (a 30px button plus an 8px gap) above that floor, so the
- // corner under it is the credit's; the controls add their own 12 on top.
- expect(layer.className).toContain('[--bottom-nav-h:calc(var(--m-map-floor)+38px)]')
+ // The band sits on that floor and the controls add their own 12 on top. It used to float
+ // a further 38px up to leave the corner under it to the credit; the phone map shows none,
+ // so that row would only be a gap over the dock.
+ expect(layer.className).toContain('[--bottom-nav-h:var(--m-map-floor)]')
+ expect(layer.className).not.toContain('38px')
// No lift off the stage: the plan tab has no bar in that band.
expect(layer.style.getPropertyValue('--m-stage-lift')).toBe('0px')
})
- it('FE-MOB-MAPAREA-013: on the stage the map floor clears the stage bar by one gap', () => {
+ it('FE-MOB-MAPAREA-013: on the stage nothing stands over the dock, so the floor stays on it', () => {
const { container } = renderArea({ trTab: 'roadtrip', mapFront: true })
- // The bar is 61px tall and keeps a gap on both sides, so the floor rises above it rather
- // than stopping on its top edge, and the credit and the round controls rise with it.
- expect((container.firstElementChild as HTMLElement).style.getPropertyValue('--m-stage-lift')).toBe('76px')
+ // A 61px stage bar used to hold this slot whenever the map was up, and the floor rose
+ // 76px to clear it. It is gone, so the road trip tab lifts nothing by itself; only the
+ // picker's bar still raises the floor (FE-MOB-MAPAREA-030).
+ expect((container.firstElementChild as HTMLElement).style.getPropertyValue('--m-stage-lift')).toBe('0px')
})
it('FE-MOB-MAPAREA-014: behind the chain there is no bar to clear, so nothing lifts', () => {
@@ -535,19 +538,39 @@ describe('MMapArea', () => {
expect(mocks.props.focusPoints).toBe(shown)
})
- it('FE-MOB-MAPAREA-026: the map layer is the credit corner on both tabs, and on the stage it rides the lift', () => {
+ it('FE-MOB-MAPAREA-036: the phone map draws no night pause, on either tab', () => {
+ // A night pause is the moon pill beside a day boundary. On the desktop it is a drag
+ // handle with a hint; a phone has neither hover nor room to drag, and the markers come
+ // from the whole drive rather than the stage, so day 1 showed day 2's pill too.
+ const nightPause = { day: 2, atPlace: false }
+ const vias = [
+ { lat: 53.55, lng: 9.99, tone: 'default' as const, nightPause },
+ { lat: 53.87, lng: 10.69, tone: 'default' as const, label: 'Rest stop' },
+ ]
+
+ const stage = renderArea(
+ { trTab: 'roadtrip', mapFront: true },
+ { ...stagePlanner(3), roadtripMapVias: vias } as unknown as Partial,
+ )
+ expect(mocks.props.routeVias).toEqual([vias[1]])
+ stage.unmount()
+
+ // The plan tab's own vias carry night pauses too, so they go through the same filter.
+ renderArea({ trTab: 'plan' }, { routeVias: vias })
+ expect(mocks.props.routeVias).toEqual([vias[1]])
+ })
+
+ it('FE-MOB-MAPAREA-026: the map layer carries no credit corner on either tab', () => {
const plan = renderArea()
const planLayer = plan.container.firstElementChild as HTMLElement
- // mobile.css places the credit off this class, in both GL engines and in Leaflet.
- expect(planLayer.classList.contains('m-credit-corner')).toBe(true)
+ // The phone map shows no credit at all now (mobile.css), so the class that used to
+ // place one is gone rather than left behind pointing at nothing.
+ expect(planLayer.classList.contains('m-credit-corner')).toBe(false)
plan.unmount()
const { container } = renderArea({ trTab: 'roadtrip', mapFront: true })
const stageLayer = container.firstElementChild as HTMLElement
- expect(stageLayer.classList.contains('m-credit-corner')).toBe(true)
- // The floor takes the same 76px the band does, so the credit lands a gap above the
- // stage bar rather than on it.
- expect(stageLayer.style.getPropertyValue('--m-stage-lift')).toBe('76px')
+ expect(stageLayer.classList.contains('m-credit-corner')).toBe(false)
})
it('FE-MOB-MAPAREA-028: on the stage the map draws the offered roads, and a tap on one only lights it', () => {
@@ -605,8 +628,8 @@ describe('MMapArea', () => {
const answered = { ...asking, ...withPicker(OFFERS) } as TripPlanner
rerender()
// Top: 12 of --m-safe-top, the rail's 50 offset and 42 height, a 12 gap. Bottom: the
- // dock's 74, the bar's lift and the 38 credit row. The sides keep the phone's 20.
- expect(mocks.props.fitPadding).toEqual({ top: 116, right: 20, bottom: 74 + RT_ALT_BAR_LIFT + 38, left: 20 })
+ // dock's 74 and the bar's lift. The sides keep the phone's 20.
+ expect(mocks.props.fitPadding).toEqual({ top: 116, right: 20, bottom: 74 + RT_ALT_BAR_LIFT, left: 20 })
const first = mocks.props.fitPadding
// Kept between renders, for the reason the stage data is.
@@ -622,13 +645,13 @@ describe('MMapArea', () => {
const style = notchedPhone('59px', '34px')
renderArea({ trTab: 'roadtrip', rtView: 'map' }, withPicker(OFFERS))
- expect(mocks.props.fitPadding).toEqual({ top: 59 + 116, right: 20, bottom: 34 + 301, left: 20 })
+ expect(mocks.props.fitPadding).toEqual({ top: 59 + 116, right: 20, bottom: 34 + 74 + RT_ALT_BAR_LIFT, left: 20 })
// Turned sideways the insets go to the edges, and the frame lets go of them.
style.mockRestore()
notchedPhone('0px', '21px')
act(() => { window.dispatchEvent(new Event('resize')) })
- expect(mocks.props.fitPadding).toEqual({ top: 116, right: 20, bottom: 21 + 301, left: 20 })
+ expect(mocks.props.fitPadding).toEqual({ top: 116, right: 20, bottom: 21 + 74 + RT_ALT_BAR_LIFT, left: 20 })
})
it('FE-MOB-MAPAREA-027: the layer sets only the lift inline, so the compass stays the one inline --bottom-nav-h', () => {
diff --git a/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx b/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
index 55a29f465..7417d35ea 100644
--- a/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
+++ b/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
@@ -498,7 +498,7 @@ describe('MRoadtripTab', () => {
describe('map half', () => {
const mapShell = () => buildShell({ rtView: 'map' })
- it('FE-MOB-RTTAB-022: renders the stage bar and nothing that would cover the map', () => {
+ it('FE-MOB-RTTAB-022: leaves the map to itself, with no bar of its own over it', () => {
const { container } = renderTab(planner(), mapShell())
expect((container.firstChild as HTMLElement).className).toContain('pointer-events-none')
@@ -506,201 +506,21 @@ describe('MRoadtripTab', () => {
expect(screen.queryByText('Shell Ebina')).toBeNull()
expect(screen.queryByText('roadtrip.refuel.dry')).toBeNull()
expect(screen.queryByText('roadtrip.window.stop')).toBeNull()
+
+ // A stage bar used to stand over the dock here, naming the day's destination with its
+ // picture, stop count, arrival and distance. The day and the distance are in the
+ // shell's stage header already, and on a map that fills the screen a permanent card
+ // over the bottom edge cost more than the three facts it added. Nothing is left of it:
+ // not the destination, not its badges, and not an empty wrapper over the map's own
+ // buttons. The picker's bar still uses that slot (FE-MOB-RTTAB-048).
+ expect(screen.queryByText('Kyoto Station')).toBeNull()
+ expect(screen.queryByText('roadtrip.day.stopCount:2')).toBeNull()
+ expect(screen.queryByText('412 km')).toBeNull()
+ expect(screen.queryByRole('button', { name: /Kyoto Station/ })).toBeNull()
+ // The search bar is still up top; what is gone is the wrapper down at the dock.
+ expect(container.querySelector('[class*="bottom-[calc(var(--bottom-nav-h"]')).toBeNull()
})
- it('FE-MOB-RTTAB-023: names the last stop of the day, and badges its count, that stop\'s own arrival and the distance', () => {
- renderTab(planner(), mapShell())
-
- // The badges run their texts together in a computed name, so the bar spells it out.
- const bar = screen.getByRole('button', { name: 'Kyoto Station, roadtrip.day.stopCount:2, 12:40, 412 km' })
- expect(within(bar).getByText('Kyoto Station')).toBeInTheDocument()
- // One badge per figure, and no middle dot left between them.
- const badges = ['roadtrip.day.stopCount:2', '12:40', '412 km'].map(text => within(bar).getByText(text).closest('.rounded-full'))
- expect(badges).not.toContain(null)
- expect(new Set(badges).size).toBe(3)
- expect(bar.textContent).not.toContain('·')
- // 12:40 is when Kyoto Station is reached. The bar used to print 22:00, the automatic
- // day end after it, which is where the window closed and not a clock of the stop named.
- expect(within(bar).queryByText(/22:00/)).toBeNull()
- // Words in caps, the unit in its own case. And neutral: the whole bar is the button,
- // and a filled pill inside it would read as a second one.
- expect(within(bar).getByText('roadtrip.day.stopCount:2').className).toContain('uppercase')
- const distance = within(bar).getByText('412 km')
- expect(distance.className).not.toContain('uppercase')
- expect(distance.className).not.toContain('bg-m-act')
- })
-
- it('FE-MOB-RTTAB-024: drops the arrival from the bar when the day has none, and the pending distance with it', () => {
- const untimed = stage({
- distance: 0,
- schedule: { entries: [0, 1, 2, 3].map(() => ({ arrival: null, departure: null, anchored: false, dayOffset: 0 })), warnings: [] },
- } as unknown as Partial)
- renderTab(planner({ roadtripRoutes: routes({ days: [untimed] }) }), mapShell())
-
- // The name leaves the missing clock out too, rather than reading an empty part or a
- // placeholder between the count and the distance.
- const bar = screen.getByRole('button', { name: 'Kyoto Station, roadtrip.day.stopCount:2, roadtrip.leg.pending' })
- expect(within(bar).getByText('roadtrip.day.stopCount:2')).toBeInTheDocument()
- expect(within(bar).getByText('roadtrip.leg.pending')).toBeInTheDocument()
- })
-
- it('FE-MOB-RTTAB-025: opens the stop the bar names, the sheet its row in the chain opens', () => {
- const { shell } = renderTab(planner(), mapShell())
-
- fireEvent.click(screen.getByRole('button', { name: /Kyoto Station/ }))
-
- expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { dayId: 2, assignmentId: 503 })
- // The bar reads as a place; the header's list switch is the way back to the chain.
- expect(shell.toggleRtView).not.toHaveBeenCalled()
- })
-
- it('FE-MOB-RTTAB-033: a stage drawn with nothing but its night marker names no stop and is not a button', () => {
- const nightOnly = stage({
- stops: [stage().stops[3]],
- legs: [],
- legVias: [],
- spills: [],
- dryPoints: [],
- driveWarnings: [],
- schedule: { entries: [{ arrival: '22:00', departure: null, anchored: false, dayOffset: 0 }], warnings: [] },
- } as unknown as Partial)
- const { shell } = renderTab(planner({ roadtripRoutes: routes({ days: [nightOnly] }) }), mapShell())
-
- const title = screen.getByText('roadtrip.stop.none')
- // Nothing for a tap to open, so a plain block rather than a dead button, and the night
- // marker's clock is not a stop's arrival either.
- expect(title.closest('button')).toBeNull()
- expect(screen.getByText('roadtrip.day.stopCount:0')).toBeInTheDocument()
- expect(screen.queryByText(/22:00/)).toBeNull()
- fireEvent.click(title)
- expect(shell.openSheet).not.toHaveBeenCalled()
- expect(shell.toggleRtView).not.toHaveBeenCalled()
- })
-
- it('FE-MOB-RTTAB-034: a stage that ends on a stop reached after midnight opens it on the day it is stored on', () => {
- // Only the spilled stop is left on the card: it is drawn here and stored on day 1.
- const spilledOnly = stage({
- stops: [stage().stops[0]],
- legs: [],
- legVias: [],
- dryPoints: [],
- driveWarnings: [],
- schedule: { entries: [{ arrival: '00:40', departure: '01:25', anchored: false, dayOffset: 0 }], warnings: [] },
- } as unknown as Partial)
- const { shell } = renderTab(planner({ roadtripRoutes: routes({ days: [spilledOnly] }) }), mapShell())
-
- const bar = screen.getByRole('button', { name: /Fuji Viewpoint/ })
- expect(within(bar).getByText('roadtrip.day.stopCount:1')).toBeInTheDocument()
- expect(within(bar).getByText('00:40')).toBeInTheDocument()
- fireEvent.click(bar)
- expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { dayId: 1, assignmentId: 501 })
- })
-
- it('FE-MOB-RTTAB-026: falls back to the whole drive when no day is picked, and offers nothing to tap', () => {
- renderTab(planner({ selectedDayId: null }), mapShell())
-
- expect(screen.getByText('roadtrip.title')).toBeInTheDocument()
- expect(screen.getByText('980 km')).toBeInTheDocument()
- // Not a dead button: with no stage there is nothing for a tap to open, so the
- // whole-drive line is a plain block. The search bar above it stays a button.
- expect(screen.getByText('roadtrip.title').closest('button')).toBeNull()
- })
-
- it('FE-MOB-RTTAB-035: pictures the place the day ends at, from the trip store and not the filtered day lists', () => {
- seedStore(useTripStore, { places: [PLACES[0], { ...PLACES[1], image_url: 'https://img.test/kyoto.jpg' }] })
- // planner.places is the list the phone takes hidden service stops out of, so the
- // picture must not depend on it: a day can end at a hidden campsite.
- renderTab(planner({ places: [] }), mapShell())
-
- const bar = screen.getByRole('button', { name: /Kyoto Station/ })
- const img = bar.querySelector('img') as HTMLImageElement
- expect(img).toHaveAttribute('src', 'https://img.test/kyoto.jpg')
- // Out of the name: the photo's alt text is the title a second time.
- expect(img.closest('[aria-hidden="true"]')).not.toBeNull()
- expect(bar).toHaveAccessibleName('Kyoto Station, roadtrip.day.stopCount:2, 12:40, 412 km')
- expect(bar.querySelector('.lucide-map-pin')).toBeNull()
- })
-
- it('FE-MOB-RTTAB-036: shows a neutral pin when no place row stands behind the stop', () => {
- seedStore(useTripStore, { places: [] })
- renderTab(planner(), mapShell())
-
- const bar = screen.getByRole('button', { name: /Kyoto Station/ })
- expect(bar.querySelector('img')).toBeNull()
- expect(bar.firstElementChild?.querySelector('.lucide-map-pin')).not.toBeNull()
- })
-
- it('FE-MOB-RTTAB-037: a place without a photo is pictured by its category', () => {
- seedStore(useTripStore, { places: [{ ...PLACES[1], category_id: 5 }] })
- const categories = [{ id: 5, name: 'Station', color: '#123456', icon: 'Train' }]
- renderTab(planner({ categories } as unknown as Partial), mapShell())
-
- const ring = screen.getByRole('button', { name: /Kyoto Station/ }).firstElementChild as HTMLElement
- expect(ring.querySelector('img')).toBeNull()
- expect(ring.firstElementChild).toHaveStyle({ backgroundColor: '#123456' })
- })
-
- it('FE-MOB-RTTAB-038: rings the picture in the day colour only once the day colours are on', () => {
- const plain = renderTab(planner(), mapShell())
- const plainRing = screen.getByRole('button', { name: /Kyoto Station/ }).firstElementChild as HTMLElement
- expect(plainRing).toHaveAttribute('aria-hidden', 'true')
- expect(plainRing.style.borderColor).toBe('')
- plain.unmount()
-
- mocks.prefs = { roadtrip_day_colors: true }
- renderTab(planner(), mapShell())
- const ring = screen.getByRole('button', { name: /Kyoto Station/ }).firstElementChild as HTMLElement
- // dayColor(2).line, the second of the eight, see dayColors.ts.
- expect(ring).toHaveStyle({ borderColor: '#ff9f0a' })
- })
-
- it('FE-MOB-RTTAB-039: a new stage never keeps the picture of the one before', () => {
- seedStore(useTripStore, { places: [PLACES[0], { ...PLACES[1], image_url: 'https://img.test/kyoto.jpg' }] })
- const shell = mapShell()
- const view = renderTab(planner(), shell)
- expect(screen.getByRole('button', { name: /Kyoto Station/ }).querySelector('img')).not.toBeNull()
-
- // The next stage ends at Fuji Viewpoint, which has no photo of its own. The avatar
- // keeps the photo it was first handed until it remounts, which is what its key is for.
- const fujiOnly = stage({
- stops: [stage().stops[0]],
- legs: [],
- legVias: [],
- dryPoints: [],
- driveWarnings: [],
- schedule: { entries: [{ arrival: '00:40', departure: '01:25', anchored: false, dayOffset: 0 }], warnings: [] },
- } as unknown as Partial)
- view.rerender()
-
- const bar = screen.getByRole('button', { name: /Fuji Viewpoint/ })
- expect(bar.querySelector('img')).toBeNull()
- })
-
- it('FE-MOB-RTTAB-040: the head card and the stage bar agree on when the day arrives', () => {
- const list = renderTab()
- const arrive = screen.getByText('roadtrip.stay.arrive').parentElement as HTMLElement
- expect(within(arrive).getByText('12:40')).toBeInTheDocument()
- list.unmount()
-
- renderTab(planner(), mapShell())
- const bar = screen.getByRole('button', { name: /Kyoto Station/ })
- // Same clock, and it keeps its digit order in an RTL locale like the head card's.
- expect(within(bar).getByText('12:40')).toHaveAttribute('dir', 'ltr')
- })
-
- it('FE-MOB-RTTAB-041: the whole-drive bar badges a measured total and leaves a total of nothing out', () => {
- const measured = renderTab(planner({ selectedDayId: null }), mapShell())
- const total = screen.getByText('980 km')
- expect(total.className).toContain('rounded-full')
- expect(total.className).not.toContain('uppercase')
- measured.unmount()
-
- renderTab(planner({ selectedDayId: null, roadtripRoutes: routes({ totalDistance: 0 }) }), mapShell())
- // Nothing routed yet is not a drive of 0 m.
- expect(screen.getByText('roadtrip.title')).toBeInTheDocument()
- expect(screen.queryByText('0 m')).toBeNull()
- expect(screen.getByText('roadtrip.title').parentElement?.querySelector('.rounded-full')).toBeNull()
- })
})
describe('the corridor search', () => {
@@ -819,21 +639,22 @@ describe('MRoadtripTab', () => {
expect(shell.toggleRtView).toHaveBeenCalledTimes(1)
})
- it('FE-MOB-RTTAB-048: on the map the picker takes the stage bar\'s slot and the search bar steps away', () => {
+ it('FE-MOB-RTTAB-048: on the map the picker stands over the dock and the search bar steps away', () => {
const shell = buildShell({ rtView: 'map' })
const open = renderTab(withPicker({}), shell)
const bar = screen.getByRole('region', { name: 'roadtrip.alt.title' })
expect(within(bar).getByText('roadtrip.alt.loading')).toBeInTheDocument()
- // In the very wrapper the stage bar sits in, just above the dock.
+ // Just above the dock, in the slot the stage bar used to hold the whole time.
expect((bar.parentElement as HTMLElement).className).toContain('bottom-[calc(var(--bottom-nav-h,84px)+4px)]')
- expect(screen.queryByRole('button', { name: /Kyoto Station/ })).toBeNull()
expect(screen.queryByText('roadtrip.poi.title')).toBeNull()
open.unmount()
- renderTab(planner(), shell)
+ // Closed, that slot is empty and the map has it back: the wrapper is mounted with the
+ // bar rather than left lying over the map's own buttons.
+ const { container } = renderTab(planner(), shell)
expect(screen.queryByRole('region', { name: 'roadtrip.alt.title' })).toBeNull()
- expect(screen.getByRole('button', { name: /Kyoto Station/ })).toBeInTheDocument()
+ expect(container.querySelector('[class*="bottom-[calc(var(--bottom-nav-h"]')).toBeNull()
expect(screen.getByText('roadtrip.poi.title')).toBeInTheDocument()
})
diff --git a/client/tests/unit/mobile/trip/useMRtAlternatives.test.ts b/client/tests/unit/mobile/trip/useMRtAlternatives.test.ts
index 872e0ebed..533d79693 100644
--- a/client/tests/unit/mobile/trip/useMRtAlternatives.test.ts
+++ b/client/tests/unit/mobile/trip/useMRtAlternatives.test.ts
@@ -195,8 +195,9 @@ describe('useMRtAlternatives', () => {
expect(viewer.result.current.canAsk).toBe(false)
})
- it('FE-MOB-RTALTH-009: the map lifts its floor by the bar and the gap the stage bar keeps', () => {
- // 61px stage bar plus 15 is the 76px stage lift; the same 15 between the dock and this bar.
+ it('FE-MOB-RTALTH-009: the map lifts its floor by the bar and the gap it keeps from the dock', () => {
+ // The same 15px a bar standing over the dock keeps from it. This is the only such bar
+ // left: the stage bar that used to hold the slot the rest of the time is gone.
expect(RT_ALT_BAR_HEIGHT).toBe(174)
expect(RT_ALT_BAR_LIFT).toBe(189)
})