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
+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 () => {