diff --git a/client/src/mobile/screens/trip/roadtrip/MRoadtripRows.tsx b/client/src/mobile/screens/trip/roadtrip/MRoadtripRows.tsx
index 54d5b41f5..dd1bc50cf 100644
--- a/client/src/mobile/screens/trip/roadtrip/MRoadtripRows.tsx
+++ b/client/src/mobile/screens/trip/roadtrip/MRoadtripRows.tsx
@@ -31,28 +31,47 @@ export interface RowChrome {
unit: DistanceUnit
}
-/** 30px disc: a number for a destination, the kind's colour for a service stop. */
-function Disc({ row }: { row: StopRow }) {
+/**
+ * 30px disc: a number for a destination, the kind's colour for a service stop.
+ *
+ * With `onPickKind` it is also the control that switches between the two, which is what
+ * the desktop rail does with its own disc. The disc is what changes, so it is its own
+ * preview: tap the 3, pick the pump, and the 3 becomes an orange disc while everything
+ * below it renumbers. The tap is stopped here rather than allowed to reach the row, which
+ * opens the stop, and the 34px column the row gives this disc is the whole target.
+ */
+function Disc({ row, t, onPickKind }: { row: StopRow; t: TranslationFn; onPickKind?: () => void }) {
const kind = row.stop.stopType ? STOP_KIND_BY_KEY[row.stop.stopType] : undefined
- if (row.service && kind) {
- const Icon = kind.Icon
- return (
+ const face = row.service && kind
+ ? (
-
+
)
- }
+ : (
+ // Deliberately not font-geist: `.m-root .font-geist` caps the tier at Medium,
+ // which would quietly undo the bold on a number that has to read at 12px.
+
+ {row.number}
+
+ )
+
+ if (!onPickKind) return face
return (
- // Deliberately not font-geist: `.m-root .font-geist` caps the tier at Medium,
- // which would quietly undo the bold on a number that has to read at 12px.
-
- {row.number}
-
+
)
}
@@ -90,10 +109,12 @@ function warningMark(warning: ScheduleWarning, chrome: RowChrome): ReactNode {
* why it is a div with role rather than a button: the desktop needs controls nested
* inside its row, and a button inside a button is not markup.
*/
-export function RtStopRow({ row, chrome, onOpen }: {
+export function RtStopRow({ row, chrome, onOpen, onPickKind }: {
row: StopRow
chrome: RowChrome
onOpen: () => void
+ /** Absent for a traveller who may not edit places: the disc is then not a control. */
+ onPickKind?: () => void
}) {
const { t, unit } = chrome
const marks: ReactNode[] = []
@@ -119,7 +140,7 @@ export function RtStopRow({ row, chrome, onOpen }: {
onKeyDown={e => { if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onOpen() } }}
className="grid cursor-pointer items-center gap-x-[10px] py-1" style={{ gridTemplateColumns: '34px 1fr auto' }}
>
-
+
{row.stop.name}
@@ -142,6 +163,30 @@ const LEG_ICONS: Record = {
cycling: Bike,
}
+/**
+ * What a leg pill says: how far and how long, or that it is not routed yet.
+ *
+ * A segment carries both a pre-formatted text and the raw metres and seconds, and the two
+ * do not always arrive together: a segment that reaches the chain before its routing round
+ * has landed has the numbers at zero and the texts empty. Reading only the texts printed
+ * the template with nothing in its slots, so a leg showed as a bare " in " (the
+ * separator of `roadtrip.leg.driveText` with both values missing) — the screenshot is a
+ * hotel followed by a pill saying nothing.
+ *
+ * So the text is used when there is one and the numbers are formatted when there is not,
+ * which is what the desktop rail does with the same segment. With neither, the leg says
+ * it is pending, the honest answer and the one the missing-segment case already gave.
+ */
+function legText(seg: RouteSegment | undefined, { t, unit }: RowChrome): string {
+ if (!seg) return t('roadtrip.leg.pending')
+ const metres = Number.isFinite(seg.distance) ? seg.distance : 0
+ const seconds = Number.isFinite(seg.duration) ? seg.duration : 0
+ const distance = seg.distanceText || (metres > 0 ? formatDistance(metres / 1000, unit) : '')
+ const time = seg.durationText || seg.drivingText || (seconds > 0 ? formatDurationShort(seconds) : '')
+ if (!distance || !time) return t('roadtrip.leg.pending')
+ return t('roadtrip.leg.driveText', { distance, time })
+}
+
/**
* The drive between two stops. 12px rather than the desktop's 10: a four-hour leg
* deserves the space it takes.
@@ -171,9 +216,7 @@ export function RtLegRow({ seg, mode, chrome, onAlternatives, alternativesOpen =
}) {
const { t } = chrome
const Icon = mode && LEG_ICONS[mode] ? LEG_ICONS[mode] : mode?.startsWith('plugin:') ? Zap : CarFront
- const text = seg
- ? t('roadtrip.leg.driveText', { distance: seg.distanceText, time: seg.durationText ?? seg.drivingText })
- : t('roadtrip.leg.pending')
+ const text = legText(seg, chrome)
return (
{/* flex-col, not flex: in a row the dashes would stretch sideways and read as
diff --git a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
index 1e9854a0d..c3314e1e7 100644
--- a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
+++ b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
@@ -62,6 +62,10 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
const stage = rt.stage
+ // The disc is only a control for somebody who may change a place. Read here rather than
+ // inside the row so the whole chain asks once, the way `rt` and `chrome` are handled.
+ const canEditPlaces = planner.can('place_edit', planner.trip)
+
const openStop = (row: StopRow) => {
shell.openSheet('rtstop', { dayId: row.stop.ownerDayId, assignmentId: row.stop.assignmentId })
}
@@ -207,7 +211,23 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
{rt.rows.map((row, i) => {
- if (row.kind === 'stop') return openStop(row)} />
+ if (row.kind === 'stop') {
+ return (
+ openStop(row)}
+ onPickKind={canEditPlaces
+ ? () => shell.openSheet('rtkind', {
+ placeId: row.stop.placeId,
+ stopType: row.stop.stopType ?? null,
+ name: row.stop.name,
+ })
+ : undefined}
+ />
+ )
+ }
if (row.kind === 'leg') {
// Only where the desk rail offers it too (legReroutable), and with the
// card's day id, the one the desk passes: the planner finds the day each
diff --git a/client/src/mobile/screens/trip/roadtrip/MRtKindSheet.tsx b/client/src/mobile/screens/trip/roadtrip/MRtKindSheet.tsx
new file mode 100644
index 000000000..44b0d389f
--- /dev/null
+++ b/client/src/mobile/screens/trip/roadtrip/MRtKindSheet.tsx
@@ -0,0 +1,149 @@
+import { useRef, useState } from 'react'
+import { MapPin } from 'lucide-react'
+import MSheet from '../../../components/MSheet'
+import type { MTripSheetsProps } from '../MTripShell'
+import { useTranslation } from '../../../../i18n'
+import { STOP_KINDS } from '../../../../components/Roadtrip/stopKinds'
+import { FormSheetHeader } from '../sheets/PlSheetChrome'
+import type { RoadtripStopType } from '@trek/shared'
+
+/** What the chain hands over when a stop's disc is tapped. */
+interface RtKindSheetPayload {
+ placeId?: number
+ /** What the stop is now: a service kind, or null for a destination. */
+ stopType?: string | null
+ name?: string
+}
+
+/**
+ * Turning a place on the drive into a pause, and back, on the phone ('rtkind', payload
+ * { placeId, stopType, name }).
+ *
+ * Opened from the stop's own disc in the chain, which is where the desktop opens it from
+ * and for the same reason: the disc is exactly what changes. A destination wears its
+ * running number, a service stop wears its kind's icon in that kind's colour, so the
+ * control is its own preview. Tap the 3, pick the pump, and the 3 becomes an orange disc
+ * while everything below it renumbers.
+ *
+ * A sheet rather than the desktop's popover under the disc: a popover is aimed at an
+ * anchor with a mouse, and the same palette on a phone would land under the thumb that
+ * opened it. The kinds stay a palette of discs rather than a list of words, as they are
+ * on the desktop. At this size the colour and the shape are the label, and seven rows of
+ * text for seven icons would be a menu where a palette does. Each disc still carries its
+ * name for a screen reader.
+ *
+ * It writes through `planner.setRoadtripStopKind`, the very call the desktop rail makes:
+ * one field on one place, with the chain redrawing off the store as soon as it lands.
+ */
+export default function MRtKindSheet({ planner, shell }: MTripSheetsProps) {
+ const { t } = useTranslation()
+ const open = shell.sheet?.id === 'rtkind'
+ const payload = (shell.sheet?.payload ?? {}) as RtKindSheetPayload
+ const canEdit = planner.can('place_edit', planner.trip)
+
+ // Hold the last payload so the name and the current kind survive the exit animation
+ // instead of blinking out while the sheet slides away.
+ const heldRef = useRef(null)
+ if (open && typeof payload.placeId === 'number') heldRef.current = payload
+ const stop = open && typeof payload.placeId === 'number' ? payload : heldRef.current
+
+ const [saving, setSaving] = useState(false)
+ const current = stop?.stopType ?? null
+
+ /**
+ * Writes the kind and closes.
+ *
+ * Closing rather than reopening the stop sheet: the answer to "what kind is this" is
+ * the disc in the chain, which is already on screen behind this sheet and redraws the
+ * moment the store lands. `setRoadtripStopKind` reports its own failures and resolves
+ * either way, so what the guard buys is a sheet that does not sit open pretending to
+ * save.
+ */
+ const pick = async (kind: RoadtripStopType | null) => {
+ const placeId = stop?.placeId
+ if (!canEdit || saving || placeId == null) return
+ setSaving(true)
+ try {
+ await planner.setRoadtripStopKind(placeId, kind)
+ shell.closeSheet()
+ } catch {
+ // Swallowed rather than toasted again: `setRoadtripStopKind` catches its own
+ // failures and shows the message itself, so a second one here would be the same
+ // news twice. What this catch is for is the sheet, which stays open on a write that
+ // never landed instead of closing as if it had.
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+ {stop && (
+ <>
+
+
+
+ {stop.name && (
+
{stop.name}
+ )}
+
+ {/* Wrapped rather than scrolled sideways: seven discs do not fit one phone row,
+ and a row that scrolls hides the kinds past its edge behind a gesture. */}
+
+ {STOP_KINDS.map(({ key, labelKey, Icon, color }) => {
+ const on = current === key
+ return (
+
+ )
+ })}
+
+
+ {/* Only once there is something to undo. On a destination it would be a button
+ saying "leave everything as it is". */}
+ {current && (
+
+ )}
+
+ >
+ )}
+
+ )
+}
diff --git a/client/src/mobile/screens/trip/sheets/MTripSheets.tsx b/client/src/mobile/screens/trip/sheets/MTripSheets.tsx
index a7b74a63e..6c304c84c 100644
--- a/client/src/mobile/screens/trip/sheets/MTripSheets.tsx
+++ b/client/src/mobile/screens/trip/sheets/MTripSheets.tsx
@@ -25,6 +25,7 @@ import MExportSheet from './MExportSheet'
import MMehrSheet from './MMehrSheet'
import MRtStopSheet from '../roadtrip/MRtStopSheet'
import MRtStaySheet from '../roadtrip/MRtStaySheet'
+import MRtKindSheet from '../roadtrip/MRtKindSheet'
import MRtInfoSheet from '../roadtrip/MRtInfoSheet'
import MRtCorridorSheet from '../roadtrip/MRtCorridorSheet'
import MRtDraftSheet from '../roadtrip/MRtDraftSheet'
@@ -72,6 +73,7 @@ export default function MTripSheets({ planner, shell }: MTripSheetsProps) {
sheet hangs off planner.stopDraft the way the place editor hangs off its flag. */}
+
{/* Before the draft sheet, not after: both sit at the same z, so the one mounted
later paints on top, and taking a hit onto the trip opens the draft OVER the
diff --git a/client/tests/unit/mobile/trip/MRoadtripRows.test.tsx b/client/tests/unit/mobile/trip/MRoadtripRows.test.tsx
index 1d17d098a..c5972cc3b 100644
--- a/client/tests/unit/mobile/trip/MRoadtripRows.test.tsx
+++ b/client/tests/unit/mobile/trip/MRoadtripRows.test.tsx
@@ -9,7 +9,7 @@ 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-040
+// FE-MOB-RTROW-001 to FE-MOB-RTROW-046
// Same echo strategy as tests/helpers/mobileTrip: assertions stay on keys, not copy.
const t: TranslationFn = (key, params) =>
@@ -168,6 +168,52 @@ describe('RtStopRow', () => {
expect(screen.getByText('35 min')).toBeInTheDocument()
})
+ // The disc as the control that switches a stop between a destination and a pause, the
+ // way the desktop rail's own disc does.
+ it('FE-MOB-RTROW-041: the disc is a button that asks for the kind, and the tap stops there', () => {
+ const onOpen = vi.fn()
+ const onPickKind = vi.fn()
+ render()
+
+ const disc = screen.getByRole('button', { name: 'roadtrip.stop.makeService' })
+ expect(disc).toHaveTextContent('2')
+
+ fireEvent.click(disc)
+
+ expect(onPickKind).toHaveBeenCalledTimes(1)
+ // The row opens the stop; a tap meant for the disc must not do both.
+ expect(onOpen).not.toHaveBeenCalled()
+ })
+
+ it('FE-MOB-RTROW-042: the disc of a service stop asks the same question, named for what it already is', () => {
+ const onPickKind = vi.fn()
+ render(
+ ,
+ )
+
+ const disc = screen.getByRole('button', { name: 'roadtrip.stop.kind' })
+ // Still the kind's own symbol: the control is its own preview of what it changes.
+ expect(disc.querySelector('.lucide-fuel')).not.toBeNull()
+
+ fireEvent.click(disc)
+ expect(onPickKind).toHaveBeenCalledTimes(1)
+ })
+
+ it('FE-MOB-RTROW-043: without the handler the disc is not a button at all', () => {
+ // A traveller who may not edit places gets no handler, and a disabled-looking control
+ // for something they cannot do is worse than no control.
+ render()
+
+ expect(screen.queryByRole('button', { name: 'roadtrip.stop.makeService' })).toBeNull()
+ expect(screen.queryByRole('button', { name: 'roadtrip.stop.kind' })).toBeNull()
+ expect(screen.getByText('2')).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()
@@ -204,6 +250,49 @@ describe('RtLegRow', () => {
expect(screen.getByText('roadtrip.leg.pending')).toBeInTheDocument()
})
+ it('FE-MOB-RTROW-044: a segment with no texts yet is read off its numbers, the way the desktop rail reads it', () => {
+ // A segment can reach the chain before its routing round has landed: metres and
+ // seconds are there, the pre-formatted texts are not.
+ render(
+ ,
+ )
+
+ expect(screen.getByText('roadtrip.leg.driveText:210 km,2 h 40 min')).toBeInTheDocument()
+ })
+
+ it('FE-MOB-RTROW-045: a segment with neither texts nor numbers says pending, never a bare separator', () => {
+ // This printed the template with both slots empty, so the pill read " in " and said
+ // nothing at all. A leg that is not routed says so.
+ render(
+ ,
+ )
+
+ expect(screen.getByText('roadtrip.leg.pending')).toBeInTheDocument()
+ expect(screen.queryByText(/driveText/)).toBeNull()
+ })
+
+ it('FE-MOB-RTROW-046: half a segment is pending too, rather than a figure with a hole beside it', () => {
+ // A distance with no time would read "210 km in", which is worse than saying nothing:
+ // it looks like a sentence that was cut off.
+ render(
+ ,
+ )
+
+ expect(screen.getByText('roadtrip.leg.pending')).toBeInTheDocument()
+ })
+
it('FE-MOB-RTROW-017: swaps the icon for a walked leg and for a plugin mode', () => {
const walk = render()
expect(walk.container.querySelector('.lucide-footprints')).not.toBeNull()
diff --git a/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx b/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
index 7417d35ea..e5119f827 100644
--- a/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
+++ b/client/tests/unit/mobile/trip/MRoadtripTab.test.tsx
@@ -230,6 +230,28 @@ describe('MRoadtripTab', () => {
expect(shell.openSheet).toHaveBeenCalledWith('rtstop', { dayId: 2, assignmentId: 503 })
})
+ it('FE-MOB-RTTAB-051: the disc asks which kind of stop it is, without opening the stop', () => {
+ const { shell } = renderTab()
+
+ // Kyoto Station is a destination, so its disc offers to make it a stop on the way.
+ fireEvent.click(screen.getAllByRole('button', { name: 'roadtrip.stop.makeService' })[1])
+
+ expect(shell.openSheet).toHaveBeenCalledWith('rtkind', {
+ placeId: 103,
+ stopType: null,
+ name: 'Kyoto Station',
+ })
+ // The row's own tap opens the stop; the disc's must not do both.
+ expect(shell.openSheet).toHaveBeenCalledTimes(1)
+ })
+
+ it('FE-MOB-RTTAB-052: a traveller who may not edit places gets a disc that is not a control', () => {
+ renderTab(planner({ can: () => false }))
+
+ expect(screen.queryByRole('button', { name: 'roadtrip.stop.makeService' })).toBeNull()
+ expect(screen.queryByRole('button', { name: 'roadtrip.stop.kind' })).toBeNull()
+ })
+
it('FE-MOB-RTTAB-006: writes the day warning out with the sentence the desktop hides in a tooltip', () => {
const over = stage({ dayWarning: { code: 'dayDriving', minutes: 620, limitMinutes: 540 } })
renderTab(planner({ roadtripRoutes: routes({ days: [over] }) }))
diff --git a/client/tests/unit/mobile/trip/MRtKindSheet.test.tsx b/client/tests/unit/mobile/trip/MRtKindSheet.test.tsx
new file mode 100644
index 000000000..b76a87e41
--- /dev/null
+++ b/client/tests/unit/mobile/trip/MRtKindSheet.test.tsx
@@ -0,0 +1,122 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import MRtKindSheet from '../../../../src/mobile/screens/trip/roadtrip/MRtKindSheet'
+import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
+import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
+import { resetAllStores } from '../../../helpers/store'
+import { fireEvent, render, screen, waitFor } from '../../../helpers/render'
+
+// FE-MOB-RTKIND-001 to FE-MOB-RTKIND-008
+//
+// The sheet renders inside the real TranslationProvider, so the copy is asserted in
+// English. It writes through the very call the desktop rail makes, so what these hold is
+// the phone's half: which stop, which kind, and that a second tap undoes it.
+
+function makePlanner(overrides: Record = {}) {
+ return buildPlanner({
+ tripId: 4,
+ setRoadtripStopKind: vi.fn(async () => undefined),
+ ...overrides,
+ } as unknown as Partial)
+}
+
+function makeShell(overrides: Record = {}) {
+ return buildShell({
+ sheet: { id: 'rtkind', payload: { placeId: 202, stopType: null, name: 'Bremen Marktplatz' } },
+ ...overrides,
+ } as unknown as Partial)
+}
+
+function renderSheet(plannerOverrides: Record = {}, shellOverrides: Record = {}) {
+ const planner = makePlanner(plannerOverrides)
+ const shell = makeShell(shellOverrides)
+ render()
+ return { planner, shell }
+}
+
+describe('MRtKindSheet', () => {
+ beforeEach(() => {
+ resetAllStores()
+ })
+
+ it('FE-MOB-RTKIND-001: stays closed while another sheet id is active', () => {
+ renderSheet({}, { sheet: { id: 'rtstop', payload: { placeId: 202 } } })
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ })
+
+ it('FE-MOB-RTKIND-002: opens on the stop it was asked about, with every kind offered', () => {
+ renderSheet()
+
+ expect(screen.getByRole('dialog', { name: 'Kind of stop' })).toBeInTheDocument()
+ expect(screen.getByText('Bremen Marktplatz')).toBeInTheDocument()
+ // The seven the desktop palette offers, each named for a screen reader although the
+ // disc itself is the label on screen.
+ for (const name of ['Accommodation', 'Fuel', 'Charging', 'Rest area', 'Campsite', 'Food', 'Sights']) {
+ expect(screen.getByRole('button', { name, pressed: false })).toBeInTheDocument()
+ }
+ })
+
+ it('FE-MOB-RTKIND-003: picking a kind writes it to the place and closes', async () => {
+ const { planner, shell } = renderSheet()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Fuel' }))
+
+ await waitFor(() => expect(planner.setRoadtripStopKind).toHaveBeenCalledWith(202, 'fuel'))
+ await waitFor(() => expect(shell.closeSheet).toHaveBeenCalledTimes(1))
+ })
+
+ it('FE-MOB-RTKIND-004: the kind a stop already is shows as chosen, and tapping it again takes it back', async () => {
+ const { planner } = renderSheet({}, {
+ sheet: { id: 'rtkind', payload: { placeId: 202, stopType: 'fuel', name: 'Bremen Marktplatz' } },
+ })
+
+ expect(screen.getByRole('button', { name: 'Fuel', pressed: true })).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Fuel' }))
+
+ // Null is what turns a pause back into a destination, and it is the same write.
+ await waitFor(() => expect(planner.setRoadtripStopKind).toHaveBeenCalledWith(202, null))
+ })
+
+ it('FE-MOB-RTKIND-005: the way back is offered only once there is something to undo', async () => {
+ renderSheet()
+ // On a destination it would be a button saying "leave everything as it is".
+ expect(screen.queryByRole('button', { name: 'Back to a destination' })).toBeNull()
+
+ const { planner } = renderSheet({}, {
+ sheet: { id: 'rtkind', payload: { placeId: 202, stopType: 'rest_area', name: 'Bremen Marktplatz' } },
+ })
+
+ fireEvent.click(screen.getAllByRole('button', { name: 'Back to a destination' })[0])
+
+ await waitFor(() => expect(planner.setRoadtripStopKind).toHaveBeenCalledWith(202, null))
+ })
+
+ it('FE-MOB-RTKIND-006: a traveller who may not edit places can open it but change nothing', () => {
+ const { planner } = renderSheet({ can: () => false })
+
+ for (const name of ['Fuel', 'Accommodation']) {
+ expect(screen.getByRole('button', { name })).toBeDisabled()
+ }
+
+ fireEvent.click(screen.getByRole('button', { name: 'Fuel' }))
+ expect(planner.setRoadtripStopKind).not.toHaveBeenCalled()
+ })
+
+ it('FE-MOB-RTKIND-007: a payload without a place is not a sheet', () => {
+ renderSheet({}, { sheet: { id: 'rtkind', payload: {} } })
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ })
+
+ it('FE-MOB-RTKIND-008: a failing write leaves the sheet open rather than claiming it saved', async () => {
+ const { shell } = renderSheet({
+ setRoadtripStopKind: vi.fn(async () => { throw new Error('offline') }),
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: 'Fuel' }))
+
+ // The planner's own call reports the failure; what matters here is that the sheet does
+ // not close on a write that never landed.
+ await waitFor(() => expect(screen.getByRole('dialog', { name: 'Kind of stop' })).toBeInTheDocument())
+ expect(shell.closeSheet).not.toHaveBeenCalled()
+ })
+})