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. */} - - - {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) })