From 01ddcc63bb6315c634675a89c6bb1a3e675db33e Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 12:31:57 +0100 Subject: [PATCH] feat(mobile): two calm round controls a side on the map, no extra buttons The phone map stacked four controls on the right and one on the left, in three different looks. Now each side has two at most, all in one frosted style, 20px off the edge and 8px apart: the whole-trip overview and locate on the right, the base layer and the Dawarich route on the left, with the compass appearing only while the map is turned off north. The map's own places filter button and sheet are gone: the places tab carries the same filter, and the map's pins follow it. The jump-to-today button beside the day rail is gone too, since today's chip is already marked. Also fixes a leftover of the segmented-control change that broke the parse. --- client/src/components/Map/LocationButton.tsx | 9 +- .../components/Map/MapCompassPill.test.tsx | 16 ++- client/src/components/Map/MapCompassPill.tsx | 9 +- client/src/mobile/components/MSegmented.tsx | 3 - client/src/mobile/screens/trip/MTripShell.tsx | 28 +--- .../src/mobile/screens/trip/map/MMapArea.tsx | 56 ++++---- .../screens/trip/map/MPlacesFilterPill.tsx | 31 ----- .../trip/places/MPlacesFilterSheet.tsx | 96 ------------- .../screens/trip/sheets/MTripSheets.tsx | 8 -- .../tests/unit/mobile/trip/MMapArea.test.tsx | 32 +---- .../mobile/trip/MPlacesFilterSheet.test.tsx | 128 ------------------ .../unit/mobile/trip/MTripSheets.test.tsx | 13 +- .../unit/mobile/trip/MTripShell.test.tsx | 18 +-- 13 files changed, 73 insertions(+), 374 deletions(-) delete mode 100644 client/src/mobile/screens/trip/map/MPlacesFilterPill.tsx delete mode 100644 client/src/mobile/screens/trip/places/MPlacesFilterSheet.tsx delete mode 100644 client/tests/unit/mobile/trip/MPlacesFilterSheet.test.tsx diff --git a/client/src/components/Map/LocationButton.tsx b/client/src/components/Map/LocationButton.tsx index f2591b192..d5fe5f4e2 100644 --- a/client/src/components/Map/LocationButton.tsx +++ b/client/src/components/Map/LocationButton.tsx @@ -69,14 +69,19 @@ export default function LocationButton({ mode, error, errorCode, onClick, bottom style={{ position: 'absolute', bottom: bottomOffset, - right: 12, + // The map's base-layer switcher sits 20px off the left edge; this mirrors it. + right: 20, zIndex: 1000, width: 42, height: 42, borderRadius: '50%', border: 'none', cursor: 'pointer', - background: isActive ? '#3b82f6' : 'var(--bg-card, white)', + // Off, it wears the same frosted glass as the map's other round controls; + // on, the location blue says it is tracking. + background: isActive ? '#3b82f6' : 'var(--sidebar-bg)', + backdropFilter: isActive ? undefined : 'blur(20px) saturate(180%)', + WebkitBackdropFilter: isActive ? undefined : 'blur(20px) saturate(180%)', color: isActive ? 'white' : (error ? '#ef4444' : 'var(--text-muted, #6b7280)'), boxShadow: MAP_CONTROL_SHADOW, display: 'flex', diff --git a/client/src/components/Map/MapCompassPill.test.tsx b/client/src/components/Map/MapCompassPill.test.tsx index 23128f0da..9a77479a7 100644 --- a/client/src/components/Map/MapCompassPill.test.tsx +++ b/client/src/components/Map/MapCompassPill.test.tsx @@ -1,4 +1,4 @@ -// FE-COMP-MAPCOMPASS-001 to FE-COMP-MAPCOMPASS-006 +// FE-COMP-MAPCOMPASS-001 to FE-COMP-MAPCOMPASS-007 import React from 'react' import { describe, it, expect, vi } from 'vitest' import { render, screen, fireEvent, act } from '../../../tests/helpers/render' @@ -86,3 +86,17 @@ describe('MapCompassPill', () => { expect(button.style.background).toBe('transparent') }) }) + +describe('MapCompassPill on the phone map', () => { + it('FE-COMP-MAPCOMPASS-007: appears only while the map is turned off north', () => { + const { map, rotateTo } = fakeCompassMap(0) + render() + expect(screen.queryByRole('button', { name: 'Reset north' })).not.toBeInTheDocument() + + rotateTo(30) + expect(screen.getByRole('button', { name: 'Reset north' })).toBeInTheDocument() + + rotateTo(0) + expect(screen.queryByRole('button', { name: 'Reset north' })).not.toBeInTheDocument() + }) +}) diff --git a/client/src/components/Map/MapCompassPill.tsx b/client/src/components/Map/MapCompassPill.tsx index 056018676..d83699cd8 100644 --- a/client/src/components/Map/MapCompassPill.tsx +++ b/client/src/components/Map/MapCompassPill.tsx @@ -15,7 +15,12 @@ export interface CompassMap { * (GL only) and built as the SAME frosted shell (padding 4 around a 34px button) * so its height and transparency match the POI pill exactly. */ -export function MapCompassPill({ map }: { map: CompassMap }) { +export function MapCompassPill({ map, onlyWhenRotated = false }: { + map: CompassMap + /** Shown only while the map is turned off north, the way a native map's compass + * appears when it has something to undo (the phone map, where room is short). */ + onlyWhenRotated?: boolean +}) { const [bearing, setBearing] = useState(() => map.getBearing()) useEffect(() => { @@ -25,6 +30,8 @@ export function MapCompassPill({ map }: { map: CompassMap }) { return () => { map.off('rotate', update) } }, [map]) + if (onlyWhenRotated && Math.abs(bearing) < 0.5) return null + return (
({ const trackRef = useRef(null) const pill = useActivePill(trackRef, '[data-active="true"]', [value, options.length]) - return () => ro.disconnect() - }, [value, options.length]) - return (
mobileTabs.find(tab => tab.id === id)?.label ?? id - // While the trip runs, today's chip carries a ring and a button jumps back to it - // from wherever the rail was scrolled (#2392). Null outside the trip's dates. + // While the trip runs, today's chip carries a ring and a dot (#2392). That mark + // is the whole of it: a second button beside the rail only crowded the chrome. + // Null outside the trip's dates. const todayDayId = findTodayDayId(days) - const jumpToToday = () => { - if (todayDayId == null || todayDayId === planner.selectedDayId) return - if (view === 'map') selectDayOnMap(todayDayId) - else planner.handleSelectDay(todayDayId, true) - } const onDayChipTap = (dayId: number) => { if (dayId === planner.selectedDayId) openSheet('day', { dayId }) @@ -542,20 +538,6 @@ export default function MTripShell({ ) })}
- {/* Back to today from anywhere on the rail (#2392). Mounted for the whole - running trip and dimmed while today is already open, so it never - appears under the thumb and shoves the rail aside. */} - {todayDayId != null && ( - - )} {/* Drop the day filter and show the whole trip (#2257). Map only: the plan timeline and the road trip chain are both single-day, so there is nothing to widen there. Stays mounted while a day is active rather than diff --git a/client/src/mobile/screens/trip/map/MMapArea.tsx b/client/src/mobile/screens/trip/map/MMapArea.tsx index 299b5f479..0e973ba91 100644 --- a/client/src/mobile/screens/trip/map/MMapArea.tsx +++ b/client/src/mobile/screens/trip/map/MMapArea.tsx @@ -4,7 +4,6 @@ import { MapCompassPill, type CompassMap } from '../../../../components/Map/MapC import { MAP_LAYER_SWITCHER_INSET, MAP_ROUND_CONTROL_SIZE } from '../../../../components/Map/MapLayerSwitcher' import { TripRouteOverviewPill, TripRouteOverviewPanel } from '../../../../components/Map/TripRouteOverview' import { DawarichTrailPill } from '../../../../components/Map/DawarichTrailPill' -import { MPlacesFilterPill } from './MPlacesFilterPill' import PoiCategoryPill from '../../../../components/Map/PoiCategoryPill' import { usePoiExplore } from '../../../../components/Map/usePoiExplore' import { useMergedMapPois } from '../../../../components/Map/useMergedMapPois' @@ -453,27 +452,38 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
)} - {/* Compass, GL maps only (Leaflet cannot rotate). Both engines draw the base-layer - switcher in the bottom left corner, so the compass stands on top of it, on the - same left edge: beside it the two read as one crowded row next to the dock. - The offsets are inline because they are computed from the switcher's own - numbers. */} - {mapActive && glMap && ( -
- + {/* The left column, over the base-layer switcher both engines draw in the bottom + left corner: the recorded Dawarich route, and the compass, which only shows + while the map is turned off north. Two round controls a side at most, one + frosted style and one 8px gap, so neither corner crowds the dock. The offsets + are inline because they are computed from the switcher's own numbers. */} + {mapActive && (glMap || planner.dawarichEnabled) && ( +
+ {/* Kept in road-trip mode: the recorded route beside the planned one is + exactly the comparison a drive invites. */} + {!onStage && planner.dawarichEnabled && ( + + )} + {glMap && }
)} - {/* Whole-trip overview (#1736): the stages stack above their toggle on the right, - clear of the round-controls band below it and of the base-layer switcher and - the compass, which both sit bottom left. The offset is Tailwind rather than - inline because the compass band is identified by being the one element with - an inline --bottom-nav-h, and a second would make that ambiguous. The places - filter closes the stack, under the toggles, so the overview card stays - right above its own toggle: it decides which pins the plan map shows, so it is - there whatever else is; the stage draws its own pins and has no use for it. */} + {/* The right column: the whole-trip overview (#1736) over the locate button, its + stages stacking above their toggle. 20px off the edge and 8px over the locate + button, mirroring the left column. The places filter has no button here: it + is set in the places tab, whose filter the map's pins follow. The offset is + Tailwind rather than inline because the compass band is identified by being + the one element with an inline --bottom-nav-h, and a second would make that + ambiguous. */} {mapActive && !onStage && ( -
+
{!planner.roadtripActive && planner.overviewActive && ( )} - {/* Kept in road-trip mode: the recorded route beside the planned one is - exactly the comparison a drive invites. */} - {planner.dawarichEnabled && ( - - )} - shell.openSheet('placesFilter')} />
)}
diff --git a/client/src/mobile/screens/trip/map/MPlacesFilterPill.tsx b/client/src/mobile/screens/trip/map/MPlacesFilterPill.tsx deleted file mode 100644 index 4a57109dd..000000000 --- a/client/src/mobile/screens/trip/map/MPlacesFilterPill.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { SlidersHorizontal } from 'lucide-react' -import { MapTogglePill } from '../../../../components/Map/MapTogglePill' -import { useTripStore } from '../../../../store/tripStore' -import { countActivePlacesFilters } from '../../../../utils/placesFilter' -import { useTranslation } from '../../../../i18n' - -/** - * The phone map's way into the places filter: a round control in the right-hand - * stack, lit and badged with how many filters are narrowing the markers. The sheet - * it opens is the shell's 'placesFilter' sheet, so the button announces a dialog - * rather than a pressed state. - */ -export function MPlacesFilterPill({ onOpen }: { onOpen: () => void }) { - const { t } = useTranslation() - const filter = useTripStore(s => s.placesFilter) - const categoryFilters = useTripStore(s => s.placesCategoryFilter) - const ratingFilter = useTripStore(s => s.placesRatingFilter) - const count = countActivePlacesFilters({ filter, categoryFilters, ratingFilter }) - const label = count > 0 ? `${t('places.filters')} (${count})` : t('places.filters') - return ( - 0} - onToggle={onOpen} - label={label} - badge={count} - testId="places-filter-pill" - opensDialog - icon={} - /> - ) -} diff --git a/client/src/mobile/screens/trip/places/MPlacesFilterSheet.tsx b/client/src/mobile/screens/trip/places/MPlacesFilterSheet.tsx deleted file mode 100644 index c25998d23..000000000 --- a/client/src/mobile/screens/trip/places/MPlacesFilterSheet.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { RotateCcw, X } from 'lucide-react' -import MSheet from '../../../components/MSheet' -import MIconBtn from '../../../components/MIconBtn' -import MChip from '../../../components/MChip' -import { useTripStore } from '../../../../store/tripStore' -import { countActivePlacesFilters } from '../../../../utils/placesFilter' -import { useTranslation } from '../../../../i18n' -import type { Category, Place } from '../../../../types' -import { MCategoryFilterList, MRatingFloorChips } from './MPlacesFilterControls' -import { placesFilterTabs, poolHasTracks } from '../../../../components/Planner/usePlacesPool' - -interface MPlacesFilterSheetProps { - open: boolean - onClose: () => void - places: Place[] - categories: Category[] - toursEnabled: boolean -} - -/** Uppercase eyebrow over each group of the sheet. */ -function SectionTitle({ children }: { children: string }) { - return ( -
{children}
- ) -} - -/** - * The places filter, opened from the phone map: which pool to show, a minimum - * rating and the trip's categories, with one button that lifts them all. Every - * choice lands in the trip store at once, so the markers behind the sheet and the - * places list change with it (#1541). Routed as shell sheet 'placesFilter'. - */ -export default function MPlacesFilterSheet({ open, onClose, places, categories, toursEnabled }: MPlacesFilterSheetProps) { - const { t } = useTranslation() - const filter = useTripStore(s => s.placesFilter) - const categoryFilters = useTripStore(s => s.placesCategoryFilter) - const ratingFilter = useTripStore(s => s.placesRatingFilter) - const setFilter = useTripStore(s => s.setPlacesFilter) - const resetPlacesFilters = useTripStore(s => s.resetPlacesFilters) - const activeCount = countActivePlacesFilters({ filter, categoryFilters, ratingFilter }) - - const pools = placesFilterTabs(t, poolHasTracks(places, toursEnabled)) - const hasCategoryChoice = categories.length > 0 || places.some(p => p.category_id == null) - - return ( - // Opaque: the sheet opens over the map, and the default bar glass let the map's - // place names show through the chips and category rows (seen at 390px wide). - -
-
{t('places.filters')}
- - - -
- -
-
- {t('places.filterShow')} -
- {pools.map(pool => ( - setFilter(pool.id)}> - {pool.label} - - ))} -
-
- -
- {t('places.filterByRating')} - -
- - {hasCategoryChoice && ( -
- {t('categories.title')} -
- -
-
- )} -
- -
- -
-
- ) -} diff --git a/client/src/mobile/screens/trip/sheets/MTripSheets.tsx b/client/src/mobile/screens/trip/sheets/MTripSheets.tsx index fdfcfdcbf..a67bc01ee 100644 --- a/client/src/mobile/screens/trip/sheets/MTripSheets.tsx +++ b/client/src/mobile/screens/trip/sheets/MTripSheets.tsx @@ -27,7 +27,6 @@ import MMehrSheet from './MMehrSheet' import MTripSettingsSheet from './MTripSettingsSheet' import MNewTripSheet from '../../dashboard/MNewTripSheet' import MAssistantScreen from '../assistant/MAssistantScreen' -import MPlacesFilterSheet from '../places/MPlacesFilterSheet' import MRtStopSheet from '../roadtrip/MRtStopSheet' import MRtStaySheet from '../roadtrip/MRtStaySheet' import MRtKindSheet from '../roadtrip/MRtKindSheet' @@ -145,13 +144,6 @@ export default function MTripSheets({ planner, shell }: MTripSheetsProps) { onClose={shell.closeSheet} /> - {/* ── Planner-flag editors (also serve ?create= and the import review) ── */} diff --git a/client/tests/unit/mobile/trip/MMapArea.test.tsx b/client/tests/unit/mobile/trip/MMapArea.test.tsx index 07de56a65..4b1400a2f 100644 --- a/client/tests/unit/mobile/trip/MMapArea.test.tsx +++ b/client/tests/unit/mobile/trip/MMapArea.test.tsx @@ -954,39 +954,9 @@ describe('MMapArea and a booked night at the edge of the stage', () => { }) describe('MMapArea — places filter', () => { - it('FE-MOB-MAPAREA-046: the plan map offers the places filter, and a tap opens its sheet', () => { - const { shell } = renderArea({ trTab: 'plan' }) - const pill = screen.getByTestId('places-filter-pill') - expect(pill).toHaveAccessibleName('Filters') - // It opens a sheet: a dialog to announce, not an on/off state. - expect(pill).toHaveAttribute('aria-haspopup', 'dialog') - expect(pill).not.toHaveAttribute('aria-pressed') - act(() => { pill.click() }) - expect(shell.openSheet).toHaveBeenCalledWith('placesFilter') - }) - - it('FE-MOB-MAPAREA-047: the pill lights up and counts the filters narrowing the pins', () => { + it('FE-MOB-MAPAREA-046: the map has no filter button of its own; the places tab sets the filter its pins follow', () => { seedStore(useTripStore, { placesFilter: 'unplanned', placesRatingFilter: 4 }) renderArea({ trTab: 'plan' }) - const pill = screen.getByTestId('places-filter-pill') - expect(pill).toHaveClass('text-accent') - expect(pill).not.toHaveAttribute('aria-pressed') - expect(pill).toHaveAttribute('aria-haspopup', 'dialog') - expect(pill).toHaveAccessibleName('Filters (2)') - expect(screen.getByText('2')).toHaveAttribute('aria-hidden', 'true') - }) - - it('FE-MOB-MAPAREA-049: a road trip keeps the places filter on its plan map, Dawarich or not', () => { - // The stack used to stand down in road-trip mode unless Dawarich had a pill in - // it; the plan map still shows the trip's places there, so its filter stays. - renderArea({ trTab: 'plan' }, { roadtripActive: true, dawarichEnabled: false }) - expect(screen.getByTestId('places-filter-pill')).toBeInTheDocument() - }) - - it('FE-MOB-MAPAREA-048: the stage draws its own pins, so it has no places filter, and neither has a covered map', () => { - renderArea({ trTab: 'roadtrip', mapFront: true }) - expect(screen.queryByTestId('places-filter-pill')).not.toBeInTheDocument() - renderArea({ view: 'plan', mapFront: false }) expect(screen.queryByTestId('places-filter-pill')).not.toBeInTheDocument() }) }) diff --git a/client/tests/unit/mobile/trip/MPlacesFilterSheet.test.tsx b/client/tests/unit/mobile/trip/MPlacesFilterSheet.test.tsx deleted file mode 100644 index cc237db43..000000000 --- a/client/tests/unit/mobile/trip/MPlacesFilterSheet.test.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, render, screen, within } from '../../../helpers/render' -import MPlacesFilterSheet from '../../../../src/mobile/screens/trip/places/MPlacesFilterSheet' -import { useTripStore } from '../../../../src/store/tripStore' -import { resetAllStores, seedStore } from '../../../helpers/store' -import { buildPlace } from '../../../helpers/factories' -import type { Category, Place } from '../../../../src/types' - -// FE-MOB-PFSHEET-001 to FE-MOB-PFSHEET-009 - -const CATEGORIES = [ - { id: 1, name: 'Sights', color: '#123456', icon: 'landmark' }, - { id: 2, name: 'Food', color: '', icon: 'utensils' }, -] as unknown as Category[] - -const TAGGED = buildPlace({ id: 1, category_id: 1 }) -const BARE = buildPlace({ id: 2, category_id: null }) -const TRACK = buildPlace({ id: 3, category_id: 2, route_geometry: '[[1,2],[3,4]]' }) - -function renderSheet({ places = [TAGGED, BARE], categories = CATEGORIES, open = true, toursEnabled = false }: { - places?: Place[] - categories?: Category[] - open?: boolean - toursEnabled?: boolean -} = {}) { - const onClose = vi.fn() - render() - return { onClose } -} - -const group = (name: string) => screen.getByRole('group', { name }) - -describe('MPlacesFilterSheet', () => { - beforeEach(() => { - resetAllStores() - }) - - it('FE-MOB-PFSHEET-001: shows the pool, the rating floors and the categories under one title', async () => { - renderSheet() - const sheet = await screen.findByRole('dialog', { name: 'Filters' }) - expect(within(sheet).getByText('Show')).toBeInTheDocument() - expect(within(group('Show')).getAllByRole('button').map(b => b.textContent)).toEqual(['All', 'Unplanned', 'Planned']) - expect(within(group('Filter by rating')).getAllByRole('button').map(b => b.textContent)) - .toEqual(['All', '5+', '4+', '3+', '2+', '1+']) - expect(screen.getByRole('checkbox', { name: 'Sights' })).toBeInTheDocument() - expect(screen.getByRole('checkbox', { name: 'Food' })).toBeInTheDocument() - expect(screen.getByRole('checkbox', { name: 'No Category' })).toBeInTheDocument() - }) - - it('FE-MOB-PFSHEET-002: the tracks pool only shows once a place carries a track', async () => { - renderSheet({ places: [TAGGED, TRACK] }) - await screen.findByRole('dialog', { name: 'Filters' }) - expect(within(group('Show')).getByRole('button', { name: 'Tracks' })).toBeInTheDocument() - expect(screen.queryByRole('checkbox', { name: 'No Category' })).not.toBeInTheDocument() - }) - - it('hides Tracks from the map filter while Tours is enabled', async () => { - const tourTrack = buildPlace({ id: 4, route_geometry: '[[5,6],[7,8]]' }) - renderSheet({ places: [TAGGED, TRACK, tourTrack], toursEnabled: true }) - await screen.findByRole('dialog', { name: 'Filters' }) - expect(within(group('Show')).queryByRole('button', { name: 'Tracks' })).not.toBeInTheDocument() - }) - - it('FE-MOB-PFSHEET-003: picking a pool writes it to the trip store', async () => { - renderSheet() - await screen.findByRole('dialog', { name: 'Filters' }) - const show = group('Show') - expect(within(show).getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'true') - fireEvent.click(within(show).getByRole('button', { name: 'Unplanned' })) - expect(useTripStore.getState().placesFilter).toBe('unplanned') - // The chosen chip says so to a screen reader, and only that one. - expect(within(show).getByRole('button', { name: 'Unplanned' })).toHaveAttribute('aria-pressed', 'true') - expect(within(show).getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'false') - }) - - it('FE-MOB-PFSHEET-004: picking a floor writes the rating filter to the trip store', async () => { - renderSheet() - await screen.findByRole('dialog', { name: 'Filters' }) - const floors = group('Filter by rating') - fireEvent.click(within(floors).getByRole('button', { name: '3+' })) - expect(useTripStore.getState().placesRatingFilter).toBe(3) - expect(within(floors).getByRole('button', { name: '3+' })).toHaveAttribute('aria-pressed', 'true') - expect(within(floors).getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'false') - }) - - it('FE-MOB-PFSHEET-005: categories toggle in and out of the shared set, "no category" included', async () => { - renderSheet() - await screen.findByRole('dialog', { name: 'Filters' }) - fireEvent.click(screen.getByRole('checkbox', { name: 'Food' })) - fireEvent.click(screen.getByRole('checkbox', { name: 'No Category' })) - expect([...useTripStore.getState().placesCategoryFilter]).toEqual(['2', 'uncategorized']) - expect(screen.getByRole('checkbox', { name: 'Food' })).toHaveAttribute('aria-checked', 'true') - fireEvent.click(screen.getByRole('checkbox', { name: 'Food' })) - expect([...useTripStore.getState().placesCategoryFilter]).toEqual(['uncategorized']) - }) - - it('FE-MOB-PFSHEET-006: reset lifts every filter, and stays off while nothing filters', async () => { - seedStore(useTripStore, { placesFilter: 'planned', placesCategoryFilter: new Set(['1']), placesRatingFilter: 5 }) - renderSheet() - await screen.findByRole('dialog', { name: 'Filters' }) - const reset = screen.getByRole('button', { name: 'Reset' }) - expect(reset).toBeEnabled() - fireEvent.click(reset) - const s = useTripStore.getState() - expect(s.placesFilter).toBe('all') - expect(s.placesCategoryFilter.size).toBe(0) - expect(s.placesRatingFilter).toBe('all') - expect(screen.getByRole('button', { name: 'Reset' })).toBeDisabled() - }) - - it('FE-MOB-PFSHEET-007: without any category to choose, the categories section is left out', async () => { - renderSheet({ places: [TAGGED], categories: [] }) - const sheet = await screen.findByRole('dialog', { name: 'Filters' }) - expect(within(sheet).queryByText('Categories')).not.toBeInTheDocument() - expect(screen.queryByRole('checkbox')).not.toBeInTheDocument() - }) - - it('FE-MOB-PFSHEET-008: the close button hands back to the caller', async () => { - const { onClose } = renderSheet() - fireEvent.click(within(await screen.findByRole('dialog', { name: 'Filters' })).getByRole('button', { name: 'Close' })) - expect(onClose).toHaveBeenCalledTimes(1) - }) - - it('FE-MOB-PFSHEET-009: nothing is rendered while closed', () => { - renderSheet({ open: false }) - expect(screen.queryByRole('dialog', { name: 'Filters' })).not.toBeInTheDocument() - }) -}) diff --git a/client/tests/unit/mobile/trip/MTripSheets.test.tsx b/client/tests/unit/mobile/trip/MTripSheets.test.tsx index 2c939d709..d5d3edb14 100644 --- a/client/tests/unit/mobile/trip/MTripSheets.test.tsx +++ b/client/tests/unit/mobile/trip/MTripSheets.test.tsx @@ -11,7 +11,7 @@ import { useSettingsStore } from '../../../../src/store/settingsStore' import { useTripStore } from '../../../../src/store/tripStore' import { buildPlanner, buildShell } from '../../../helpers/mobileTrip' import { resetAllStores, seedStore } from '../../../helpers/store' -import { fireEvent, render, screen, waitFor, within } from '../../../helpers/render' +import { fireEvent, render, screen, waitFor } from '../../../helpers/render' // FE-MOB-SHOST-001 to FE-MOB-SHOST-029 and FE-MOB-SHOST-032 to FE-MOB-SHOST-034 (030 and 031 live in MTripSheets.members.test.tsx) // @@ -240,15 +240,8 @@ describe('MTripSheets', () => { } }) - it('FE-MOB-SHOST-033: the map\'s places filter opens for its own id and closes through the shell', async () => { - const { shell } = renderHost({}, { sheet: { id: 'placesFilter' } }) - const sheet = await screen.findByRole('dialog', { name: 'Filters' }) - fireEvent.click(within(sheet).getByRole('button', { name: 'Close' })) - expect(shell.closeSheet).toHaveBeenCalledTimes(1) - }) - - it('FE-MOB-SHOST-033b: no places filter while another sheet is open', () => { - renderHost({}, { sheet: { id: 'day' } }) + it('FE-MOB-SHOST-033: hosts no places filter of its own any more; the places tab carries it', () => { + renderHost({}, { sheet: { id: 'placesFilter' } }) expect(screen.queryByRole('dialog', { name: 'Filters' })).not.toBeInTheDocument() }) diff --git a/client/tests/unit/mobile/trip/MTripShell.test.tsx b/client/tests/unit/mobile/trip/MTripShell.test.tsx index 085e79b57..36275a99f 100644 --- a/client/tests/unit/mobile/trip/MTripShell.test.tsx +++ b/client/tests/unit/mobile/trip/MTripShell.test.tsx @@ -668,31 +668,25 @@ describe('MTripShell', () => { { id: 22, day_number: 2, date: todayIso() }, { id: 23, day_number: 3, date: shift(1) }, ] as unknown as Day[] - const jump = () => screen.getByRole('button', { name: 'mobileTrip.jumpToToday' }) + const jump = () => screen.queryByRole('button', { name: 'mobileTrip.jumpToToday' }) - it('FE-MOB-SHELL-090: marks today and jumps back to it from another day', () => { - const { planner, container } = renderShell({ days: running(), selectedDayId: 21 } as Partial) + it('FE-MOB-SHELL-090: marks today on its chip, and puts no extra button beside the rail', () => { + const { container } = renderShell({ days: running(), selectedDayId: 21 } as Partial) const marked = container.querySelectorAll('[data-today]') expect(marked).toHaveLength(1) expect(marked[0].className).toContain('ring-inset') expect(marked[0]).toHaveTextContent('mobileTrip.today') - - expect(jump()).toBeEnabled() - fireEvent.click(jump()) - expect(planner.handleSelectDay).toHaveBeenLastCalledWith(22, true) + expect(jump()).toBeNull() }) - it('FE-MOB-SHELL-091: with today open the button stays, dimmed, so the rail does not move', () => { + it('FE-MOB-SHELL-091: with today open the active chip keeps its own look, the dot says which day is today', () => { const { container } = renderShell({ days: running(), selectedDayId: 22 } as Partial) - expect(jump()).toBeDisabled() - // The active chip keeps its own look; the dot still says which day is today. expect(container.querySelector('[data-today]')?.className).not.toContain('ring-inset') }) - it('FE-MOB-SHELL-092: outside the trip dates there is no today and no button', () => { + it('FE-MOB-SHELL-092: outside the trip dates there is no today', () => { const { container } = renderShell() expect(container.querySelector('[data-today]')).toBeNull() - expect(screen.queryByRole('button', { name: 'mobileTrip.jumpToToday' })).toBeNull() }) })