fix(roadtrip, journey): six the audit turned up, each one verified before it was touched

**A saved stay left an invisible sheet standing, and the day swipe stayed dead behind it.**
The stay sheet reopened 'rtstop' by spreading its own payload, which carries a place id.
That sheet is located by day and assignment, so it resolved to nothing and drew nothing,
while `shell.sheet` still pointed at it and `menuOpen: !!shell.sheet` kept the day swipe
blocked until something else closed it. The row rides along now, both ways, and with no row
to go back to the sheet closes rather than opening one that resolves to nothing.

**"End the day here" showed off for a day that already ends there, and switching it on
deleted that day end.** Two things close a day: the stop's own flag and a manual boundary
filed against it. The writer has always known about both; the phone switch read only the
flag, so the tap ran through the writer's boundary branch and cleared the boundary. The
question is asked once now, in the planner beside that writer, and the desktop reads the
same answer it always computed inline.

**The road trip chain printed 24-hour clocks at a reader on a twelve-hour one.** The model
hands clocks over as HH:MM, which is what the schedule computes in, and the chain, the head
card and the up-next card printed it straight. The desktop rail, the day timeline and this
tab's own sheets all honour the setting; these go through the same formatter, carried in the
row chrome so no two rows can disagree.

**The up-next card's navigate button read "places.navigate".** No locale has that key. It is
`inspector.navigation`, which is what the stop sheet beside it, the place inspector and the
place sheet all use, with the single-target label when there is only one.

**Offered roads were invisible on a satellite basemap.** The imagery is anchored under the
first layer TREK draws, found by prefix, and the roads offered for a leg are added before
the route source so they sit under the current route. That also puts them before the first
`trip-` layer, so the raster went straight over them. The prefix list knows them now.

**A journal grew a day section for every day Dawarich recorded, all year.** The hook reads
every open stay the account has, and the timeline took all of their dates. A week in May
therefore sprouted sections from September. Bounded by the journal's own span now, its
entries together with the dates of the trips it links, and with no span at all it offers
nothing rather than everything.
This commit is contained in:
Maurice
2026-09-17 16:38:17 +02:00
parent 08df1812be
commit 92ccd24eed
12 changed files with 177 additions and 29 deletions
@@ -2037,6 +2037,28 @@ describe('MapViewGL', () => {
)
})
it('FE-COMP-MAPVIEWGL-100: the imagery goes under the offered roads too, not just under the route', async () => {
// The roads offered for a leg are added before the route source so they sit beneath the
// current route, which also puts them before the first `trip-` layer. Anchoring the
// imagery on that first `trip-` layer therefore painted it straight over them, and an
// offered road on a satellite basemap could not be seen at all.
loadOnAttach()
useSettingsStore.setState({
settings: { ...useSettingsStore.getState().settings, map_base_layer: 'satellite' },
} as never)
glMap.getStyle.mockReturnValue({
layers: [{ id: 'background' }, { id: 'road' }, { id: 'route-alt-hit' }, { id: 'route-alt-line' }, { id: 'trip-route' }],
})
render(<MapViewGL places={[]} fitKey={1} />)
await flushFrames()
expect(glMap.addLayer).toHaveBeenCalledWith(
expect.objectContaining({ id: 'trip-satellite-raster', type: 'raster' }),
'route-alt-hit',
)
})
it('FE-COMP-MAPVIEWGL-075: with the default basemap no imagery is fetched at all', async () => {
loadOnAttach()
useSettingsStore.setState({
+5 -1
View File
@@ -74,7 +74,11 @@ const GPX_HIT_LAYER_ID = 'trip-gpx-hit'
const SATELLITE_SOURCE_ID = 'trip-satellite'
const SATELLITE_LAYER_ID = 'trip-satellite-raster'
/** Everything TREK draws is prefixed; the imagery is inserted before the first of them. */
const OWN_LAYER_PREFIXES = ['trip-', 'trek-']
// Everything TREK draws on a GL map, so the satellite raster can be slipped underneath all
// of it. `route-alt-` earns its place here: the roads offered for a leg are added before the
// route source so they sit under the current route, which also puts them before the first
// `trip-` layer, and anchoring the imagery there painted right over them.
const OWN_LAYER_PREFIXES = ['trip-', 'trek-', 'route-alt-']
type PlaceWithCoords = Place & { lat: number; lng: number }
@@ -5,6 +5,7 @@ import MIconBtn from '../../../components/MIconBtn'
import { formatDurationShort, serviceColor } from '../../../../components/Roadtrip/roadtripModel'
import { STOP_KIND_BY_KEY } from '../../../../components/Roadtrip/stopKinds'
import { formatDistance } from '../../../../utils/units'
import { formatClockTime } from '../../../../utils/formatters'
import type { StopRow } from '../../../../components/Roadtrip/roadtripRowModel'
import type { RefuelSearch } from '../../../../components/Roadtrip/useRefuelSearch'
import { REFUEL_EMPTY_KEY, REFUEL_WORDS, refuelBandState, type RefuelCandidate } from '../../../../components/Roadtrip/refuelSuggestion'
@@ -29,6 +30,15 @@ import type { TranslationFn } from '../../../../types'
export interface RowChrome {
t: TranslationFn
unit: DistanceUnit
/**
* Whether this reader is on a twelve hour clock.
*
* The model hands every clock over as `HH:MM`, which is the shape the schedule computes
* in, and printing that straight to the screen ignored the setting the desktop rail, the
* day timeline and this tab's own sheets all honour. It rides in the chrome rather than
* being read per row: the chain draws dozens of rows and they must not disagree.
*/
is12h: boolean
}
/**
@@ -151,7 +161,7 @@ export function RtStopRow({ row, chrome, onOpen, onPickKind }: {
// dir=ltr so a clock reads the same way round in an RTL locale.
<span dir="ltr" className={`flex items-center gap-[3px] whitespace-nowrap text-[0.8125rem] tabular-nums ${row.pinned ? 'font-semibold text-m-ink' : 'font-medium text-m-faint'}`}>
{row.pinned && <Pin size={9} strokeWidth={2.4} className="flex-none text-m-faint" aria-label={t('roadtrip.stop.pinned')} />}
{row.time}
{formatClockTime(row.time, chrome.is12h)}
</span>
)}
</div>
@@ -470,7 +480,7 @@ export function RtAutoRow({ phase, time, chrome }: {
<span className="text-[0.75rem] font-semibold text-m-muted">
{phase === 'end' ? t('roadtrip.window.stop') : t('roadtrip.window.resume')}
</span>
{time && <span className="ms-auto text-[0.8125rem] font-semibold tabular-nums text-m-ink">{time}</span>}
{time && <span className="ms-auto text-[0.8125rem] font-semibold tabular-nums text-m-ink">{formatClockTime(time, chrome.is12h)}</span>}
</div>
)
}
@@ -13,6 +13,7 @@ import { formatDurationShort } from '../../../../components/Roadtrip/roadtripMod
import { getNavigationTargets } from '../../../../components/Planner/placeNavigation'
import { useSettingsStore } from '../../../../store/settingsStore'
import { formatDistance } from '../../../../utils/units'
import { formatClockTime } from '../../../../utils/formatters'
import { isRtlLanguage } from '../../../../i18n'
import type { MTripTabPanelProps } from '../MTripShell'
import { legReroutable, type StopRow } from '../../../../components/Roadtrip/roadtripRowModel'
@@ -40,7 +41,8 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
// the map read one controller, so a leg shows pressed for exactly the picker on the map.
const alts = useMRtAlternatives(planner, shell)
const unit = useSettingsStore(s => s.settings.distance_unit)
const chrome: RowChrome = { t, unit }
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
const chrome: RowChrome = { t, unit, is12h }
// The same gesture the day timeline uses, called rather than rebuilt: 370 lines of
// worked-out conflict avoidance, down to the dead 24px gutter that stops iOS from
@@ -176,7 +178,7 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
{t('mobileTrip.rtStart')}
</span>
<span dir="ltr" className="mt-0.5 block text-[1.5rem] font-extrabold leading-none tabular-nums text-m-ink">
{rt.clocks.start ?? '-'}
{rt.clocks.start ? formatClockTime(rt.clocks.start, is12h) : '-'}
</span>
</span>
<span className="min-w-0 text-right">
@@ -184,7 +186,7 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
{t('roadtrip.stay.arrive')}
</span>
<span dir="ltr" className="mt-0.5 block text-[1.5rem] font-extrabold leading-none tabular-nums text-m-ink">
{rt.clocks.arrive ?? '-'}
{rt.clocks.arrive ? formatClockTime(rt.clocks.arrive, is12h) : '-'}
</span>
</span>
</div>
@@ -291,6 +293,7 @@ function UpNext({ planner, shell, rt, stageDayId, onOpen }: {
onOpen: (row: StopRow) => void
}) {
const { t } = planner
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
const next = rt.upNext
if (!next) return null
const late = next.minutesUntil < 0
@@ -320,7 +323,7 @@ function UpNext({ planner, shell, rt, stageDayId, onOpen }: {
</span>
<span className="mt-1.5 block truncate text-[1.125rem] font-bold text-m-ink">{next.row.stop.name}</span>
{next.row.time && (
<span className="mt-[2px] block font-geist text-[0.75rem] tabular-nums text-m-muted">{next.row.time}</span>
<span className="mt-[2px] block font-geist text-[0.75rem] tabular-nums text-m-muted">{formatClockTime(next.row.time, is12h)}</span>
)}
</button>
<div className="mt-3 flex gap-2">
@@ -332,7 +335,7 @@ function UpNext({ planner, shell, rt, stageDayId, onOpen }: {
className={`flex h-11 flex-1 items-center justify-center gap-[7px] rounded-full bg-m-act text-[0.8125rem] font-semibold text-m-actfg shadow-[0_10px_24px_-10px_rgba(0,0,0,.45)] ${targets.length ? '' : 'pointer-events-none opacity-40'}`}
>
<Navigation size={15} strokeWidth={2.2} aria-hidden="true" />
{t('places.navigate')}
{targets.length === 1 ? targets[0].label : t('inspector.navigation')}
</a>
<button
type="button"
@@ -17,6 +17,9 @@ interface RtStaySheetPayload {
/** The stay the place carries right now; absent and zero mean the same thing. */
minutes?: number | null
name?: string
/** Where the stop sheet was standing, so saving can hand the traveller back to it. */
dayId?: number
assignmentId?: number
}
/** The step the two buttons move in. Same five minutes as the desktop dialog. */
@@ -141,7 +144,14 @@ export default function MRtStaySheet({ planner, shell }: MTripSheetsProps) {
setSaving(true)
try {
await planner.setRoadtripStay(placeId, value)
shell.openSheet('rtstop', { ...stop, placeId, minutes: value })
// 'rtstop' is located by day and assignment, not by place: spreading this sheet's own
// payload into it opened a sheet that resolved to nothing, drew nothing, and still
// counted as open, which blocks the day swipe until something else closes it.
if (stop?.dayId != null && stop.assignmentId != null) {
shell.openSheet('rtstop', { dayId: stop.dayId, assignmentId: stop.assignmentId })
} else {
shell.closeSheet()
}
} catch {
setMinutes(stored)
planner.toast.error(t('common.unknownError'))
@@ -179,7 +179,11 @@ export default function MRtStopSheet({ planner, shell }: MTripSheetsProps) {
* the stage sheet calls the stop-addressed writer directly.
*/
const canEndDay = stop != null && planner.dailyTimesActive && planner.can('day_edit', planner.trip)
const endDayTruth = !!stop?.endDay
// Both things that can end a day, not just the flag: a manual boundary filed against
// this stop closes the day too, and reading only the flag showed the switch as off for a
// stop that already ends it. Worse, the tap meant to switch it on then ran through the
// writer's boundary branch and deleted that day end.
const endDayTruth = stop != null && planner.roadtripEndsDayAt(stop)
// The optimistic value is released the moment the planner's own answer moves off what
// it was taken from, so a landed write shows the real state rather than a copy of it.
const endDayActive = pending && pending.from === endDayTruth ? pending.to : endDayTruth
@@ -225,7 +229,17 @@ export default function MRtStopSheet({ planner, shell }: MTripSheetsProps) {
const stayMinutes = stop.dwellMinutes
const canEditPlace = planner.can('place_edit', planner.trip)
const editStay = () => {
shell.openSheet('rtstay', { placeId: stop.placeId, minutes: stayMinutes })
// The row this sheet is on rides along, because the stay sheet comes back here when it
// saves and 'rtstop' is located by day and assignment. Without them it reopened on a
// payload it could not resolve, drew nothing, and left shell.sheet pointing at a sheet
// that is not on screen, which is what the day swipe reads to decide it is blocked.
shell.openSheet('rtstay', {
placeId: stop.placeId,
minutes: stayMinutes,
name: stop.name,
dayId: located.day.dayId,
assignmentId: stop.assignmentId,
})
}
// A negative id is the optimistic row of a stop still being saved. The editor would
+21 -9
View File
@@ -71,21 +71,33 @@ function JourneyDetailPageDesktop() {
query,
)
const dayGroups = groupByDate(timelineEntries)
// A stay falls on the day it happened, and that is often a day the journal has no entry
// on yet — which is the whole point of offering it. Those days join the timeline so the
// stay can be reached; without them the only way to a quiet day's stays would be to
// write an entry on it first. Not while a search is running: a day whose entries the
// query filtered out is not a day the reader is looking at.
const suggestionDates = canEditEntries && !query ? [...dawarichByDate.keys()] : []
const sortedDates = [...new Set([...dayGroups.keys(), ...suggestionDates])]
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
const tripDateMin = current.trips.length
? current.trips.reduce((min: string, t: any) => t.start_date && (!min || t.start_date < min) ? t.start_date : min, '')
: null
const tripDateMax = current.trips.length
? current.trips.reduce((max: string, t: any) => t.end_date && (!max || t.end_date > max) ? t.end_date : max, '')
: null
// A stay falls on the day it happened, and that is often a day the journal has no entry
// on yet, which is the whole point of offering it. Those days join the timeline so the
// stay can be reached; without them the only way to a quiet day's stays would be to
// write an entry on it first.
//
// Bounded by this journal's own span, because the hook reads every open stay the account
// has: unbounded, a week in May grew a day section for every day Dawarich recorded
// anywhere, all year. The span is the entries it already holds together with the dates of
// the trips it links, so a journal that is still empty but linked to a trip still offers
// that trip's days. With no span at all there is nothing to place a stay against.
// Not while a search is running either: a day whose entries the query filtered out is not
// a day the reader is looking at.
const spanDates = [...dayGroups.keys(), tripDateMin, tripDateMax]
.filter((d): d is string => !!d)
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
const suggestionDates = canEditEntries && !query && spanDates.length
? [...dawarichByDate.keys()].filter(d => d >= spanDates[0] && d <= spanDates[spanDates.length - 1])
: []
const sortedDates = [...new Set([...dayGroups.keys(), ...suggestionDates])]
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
const lifecycle = computeJourneyLifecycle(current.status, tripDateMin || null, tripDateMax || null)
const showMobileCombined = isMobile && view === 'timeline'
+18 -1
View File
@@ -1223,6 +1223,22 @@ export function useTripPlanner() {
*/
const [stayDraft, setStayDraft] = useState<{ placeId: number; name: string; minutes: number | null; arrival: string | null } | null>(null)
/**
* Whether the day ends at this stop, from BOTH the things that can end it.
*
* A stop carries an `end_day` flag, and a day can also be closed by a manual boundary
* filed against that stop (`to_assignment_id === null`). `setRoadtripEndDay` already
* knows about both and clears whichever is set, so a surface reading only the flag shows
* a day end as off, and the tap meant to switch it on deletes the boundary instead. The
* question is asked here once rather than answered again per surface.
*/
const roadtripEndsDayAt = useCallback(
(stop: RoadtripStop): boolean =>
!!stop.endDay
|| dayBoundaries.boundaries.some(b => b.to_assignment_id === null && b.from_assignment_id === stop.assignmentId),
[dayBoundaries.boundaries],
);
const setRoadtripEndDay = useCallback(async (stop: RoadtripStop) => {
if (!dailyTimesActive || !can('day_edit', trip)) return
try {
@@ -2396,7 +2412,7 @@ export function useTripPlanner() {
)
const endDayStop = selectedRoadtripStops.length === 1 ? selectedRoadtripStops[0] : undefined
const roadtripEndDay = roadtripActive && dailyTimesActive && can('day_edit', trip) && endDayStop && endDayStop.assignmentId > 0
? { active: !!endDayStop.endDay || dayBoundaries.boundaries.some(b => b.to_assignment_id === null && b.from_assignment_id === endDayStop.assignmentId), onToggle: () => setRoadtripEndDay(endDayStop) }
? { active: roadtripEndsDayAt(endDayStop), onToggle: () => setRoadtripEndDay(endDayStop) }
: undefined
const roadtripStay = roadtripActive && selectedPlace
? { minutes: endDayStop ? endDayStop.dwellMinutes : selectedPlace.duration_minutes ?? null, onEdit: can('place_edit', trip) ? () => editRoadtripStay({ placeId: selectedPlace.id, name: selectedPlace.name, minutes: selectedPlace.duration_minutes ?? null, arrival: null }) : undefined }
@@ -2459,6 +2475,7 @@ export function useTripPlanner() {
stopDraft, setStopDraft, saveStopDraft, saveStopDraftAsNight, stopDraftToForm, stopDraftDuplicate, reorderRoadtripStop,
setRoadtripStopKind,
setRoadtripStopFill,
roadtripEndsDayAt,
roadtripSettingsLoading: !roadtripPreferencesState.ready && !roadtripPreferencesState.failed,
saveRoadtripLimit: roadtripPreferencesState.ready && can('day_edit', trip) ? saveRoadtripLimit : undefined,
roadtripVias, addRoadtripVia, moveRoadtripVia, removeRoadtripVia, dayBoundaryControls, resetDayBoundaries,
+3
View File
@@ -169,6 +169,9 @@ export function buildPlanner(overrides: Partial<TripPlanner> = {}): TripPlanner
routeProfile: 'driving',
setRouteProfile: vi.fn(),
routeVias: [],
// Whether the day ends at a stop, from the flag and from a manual boundary alike. The
// helper answers from the flag; a case about boundaries overrides it.
roadtripEndsDayAt: (stop: { endDay?: boolean } | null | undefined) => !!stop?.endDay,
fitKey: 0,
setFitKey: vi.fn(),
@@ -9,13 +9,13 @@ import type { TranslationFn } from '../../../../src/types'
import type { RefuelSearch } from '../../../../src/components/Roadtrip/useRefuelSearch'
import type { RefuelCandidate } from '../../../../src/components/Roadtrip/refuelSuggestion'
// FE-MOB-RTROW-001 to FE-MOB-RTROW-046
// FE-MOB-RTROW-001 to FE-MOB-RTROW-048
// Same echo strategy as tests/helpers/mobileTrip: assertions stay on keys, not copy.
const t: TranslationFn = (key, params) =>
params ? `${key}:${Object.values(params).join(',')}` : key
const chrome: RowChrome = { t, unit: 'metric' }
const chrome: RowChrome = { t, unit: 'metric', is12h: false }
function stopRow(over: Partial<StopRow> = {}): StopRow {
return {
@@ -214,6 +214,22 @@ describe('RtStopRow', () => {
expect(screen.getByText('2')).toBeInTheDocument()
})
it('FE-MOB-RTROW-047: a clock follows the reader twelve hour setting', () => {
// The model hands clocks over as HH:MM, which is the shape the schedule computes in.
// Printing that straight to the screen ignored the setting the desktop rail, the day
// timeline and this tab's own sheets all honour.
render(<RtStopRow row={stopRow({ time: '14:05' })} chrome={{ ...chrome, is12h: true }} onOpen={vi.fn()} />)
expect(screen.getByText('2:05 PM')).toBeInTheDocument()
expect(screen.queryByText('14:05')).toBeNull()
})
it('FE-MOB-RTROW-048: on a twenty four hour clock it stays exactly as the model wrote it', () => {
render(<RtStopRow row={stopRow({ time: '14:05' })} chrome={chrome} onOpen={vi.fn()} />)
expect(screen.getByText('14:05')).toBeInTheDocument()
})
it('FE-MOB-RTROW-013: writes an overnight warning as a word, since it has no figure', () => {
const warning: ScheduleWarning = { index: 1, code: 'overnight' }
const { container } = render(<RtStopRow row={stopRow({ warning })} chrome={chrome} onOpen={vi.fn()} />)
@@ -50,7 +50,7 @@ function makePlanner(overrides: Record<string, unknown> = {}) {
function makeShell(overrides: Record<string, unknown> = {}) {
return buildShell({
sheet: { id: 'rtstay', payload: { placeId: 202, minutes: 30, name: 'Bremen Marktplatz' } },
sheet: { id: 'rtstay', payload: { placeId: 202, minutes: 30, name: 'Bremen Marktplatz', dayId: 11, assignmentId: 102 } },
...overrides,
} as unknown as Partial<MTripShellApi>)
}
@@ -95,7 +95,7 @@ describe('MRtStaySheet', () => {
})
it('FE-MOB-RTSTAY-004: minus stops at zero instead of going negative', () => {
renderSheet({}, { sheet: { id: 'rtstay', payload: { placeId: 202, minutes: 3, name: 'Bremen Marktplatz' } } })
renderSheet({}, { sheet: { id: 'rtstay', payload: { placeId: 202, minutes: 3, name: 'Bremen Marktplatz', dayId: 11, assignmentId: 102 } } })
fireEvent.click(minus())
expect(value()).toHaveTextContent('0')
expect(minus()).toBeDisabled()
@@ -158,7 +158,25 @@ describe('MRtStaySheet', () => {
fireEvent.click(plus())
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(planner.setRoadtripStay).toHaveBeenCalledWith(202, 35))
expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { placeId: 202, minutes: 35, name: 'Bremen Marktplatz' })
// Addressed by day and assignment, which is how 'rtstop' locates a row. Spreading this
// sheet's own payload left it unable to resolve anything, so it drew nothing while still
// counting as open, and the day swipe stayed blocked behind it.
expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { dayId: 11, assignmentId: 102 })
})
it('FE-MOB-RTSTAY-019: with no row to go back to it closes rather than opening a sheet that resolves to nothing', async () => {
// Every caller sends the row, but a payload without one must not leave `shell.sheet`
// pointing at a stop sheet that cannot locate anything: it would draw nothing and still
// count as open, and the day swipe reads exactly that to decide it is blocked.
const { planner, shell } = renderSheet({}, {
sheet: { id: 'rtstay', payload: { placeId: 202, minutes: 30, name: 'Bremen Marktplatz' } },
})
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(planner.setRoadtripStay).toHaveBeenCalledWith(202, 30))
expect(shell.closeSheet).toHaveBeenCalled()
expect(shell.openSheet).not.toHaveBeenCalledWith('rtstop', expect.anything())
})
it('FE-MOB-RTSTAY-013: clearing the stay is a save of zero, not a second write path', async () => {
@@ -168,7 +186,7 @@ describe('MRtStaySheet', () => {
expect(value()).toHaveTextContent('0')
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(planner.setRoadtripStay).toHaveBeenCalledWith(202, 0))
expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { placeId: 202, minutes: 0, name: 'Bremen Marktplatz' })
expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { dayId: 11, assignmentId: 102 })
})
it('FE-MOB-RTSTAY-014: closing without saving writes nothing and does not reopen the stop', () => {
@@ -215,10 +215,29 @@ describe('MRtStopSheet', () => {
expect(screen.queryByText('Set how full this stop fills')).not.toBeInTheDocument()
})
it('FE-MOB-RTSTOP-011: the stay tile hands the stop over to the stay sheet', () => {
it('FE-MOB-RTSTOP-011: the stay tile hands the stop over to the stay sheet, row and all', () => {
const { shell } = renderSheet()
fireEvent.click(screen.getByRole('button', { name: /^Stay/ }))
expect(shell.openSheet).toHaveBeenCalledWith('rtstay', { placeId: 203, minutes: 90 })
// The day and the assignment ride along because the stay sheet comes back here when it
// saves, and this sheet is located by those two rather than by the place. Without them
// it reopened on a payload it could not resolve and left an invisible sheet standing.
expect(shell.openSheet).toHaveBeenCalledWith('rtstay', {
placeId: 203,
minutes: 90,
name: 'Bremen Marktplatz',
dayId: 11,
assignmentId: 103,
})
})
it('FE-MOB-RTSTOP-036: a day ended by a manual boundary reads as ended, not as off', () => {
// Two things can end a day: the stop's own flag and a manual boundary filed against it.
// Reading only the flag showed the switch as off for a stop that already ends the day,
// and the tap meant to switch it on ran through the writer's boundary branch and
// deleted that day end instead.
renderSheet({ roadtripEndsDayAt: () => true })
expect(screen.getByRole('switch', { name: 'End the day here' })).toHaveAttribute('aria-checked', 'true')
})
it('FE-MOB-RTSTOP-012: ending the day here calls the stop-addressed writer with that very stop', async () => {