Files
TREK/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
T
Maurice de128cc4ae wip(i18n): plural forms through Intl.PluralRules
Unfinished, continued in the next session: t() resolves key.one/.few/...
by the language's own plural rule, the parity check knows which forms a
language needs, 16 singular/plural key pairs are merged in every locale
and every count === 1 branch in the client uses the plural form. Still
open: the forms each locale needs for the remaining count strings, the
two new labels in every locale, and 35 tests that expect the old English
wording.
2026-10-07 22:57:51 +02:00

518 lines
26 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, fireEvent, render, screen, waitFor, within } from '../../../helpers/render'
import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
import MPlacesBrowser from '../../../../src/mobile/screens/trip/places/MPlacesBrowser'
import { collectionsApi } from '../../../../src/api/collections'
import { toursApi } from '../../../../src/api/client'
import type { CollectionListResponse, TourListItem } from '@trek/shared'
import { useAddonStore } from '../../../../src/store/addonStore'
import { useTripStore } from '../../../../src/store/tripStore'
import { resetAllStores, seedStore } from '../../../helpers/store'
import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
import type { AssignmentsMap, Category, Day, Place } from '../../../../src/types'
// FE-MOB-PBROW-001 to FE-MOB-PBROW-036
const CATEGORIES = [
{ id: 1, name: 'Sights', color: '#123456', icon: 'landmark' },
// No colour — the panel and the row fall back to the muted token.
{ id: 2, name: 'Food', color: '', icon: 'utensils' },
] as unknown as Category[]
const DAYS = [
{ id: 21, trip_id: 1, day_number: 1, date: '2026-05-01' },
{ id: 22, trip_id: 1, day_number: 2, date: '2026-05-02' },
] as unknown as Day[]
function place(over: Partial<Place>): Place {
return {
id: 0, trip_id: 1, name: '', address: null, description: null, category_id: null,
lat: 48.2, lng: 16.3, image_url: null, google_place_id: null, osm_id: null,
route_geometry: null, route_color: null,
...over,
} as unknown as Place
}
const LOUVRE = place({ id: 1, name: 'Louvre', address: 'Rue de Rivoli', category_id: 1 })
const EIFFEL = place({ id: 2, name: 'Eiffel Tower', description: 'Iron lady' })
const SEINE = place({ id: 3, name: 'Seine Track', category_id: 2, route_geometry: 'abc', route_color: '#ff0000' })
const TOUR = place({ id: 4, name: 'Ridge walk', tour_place_id: 4 })
const PLACES = [LOUVRE, EIFFEL, SEINE]
// Louvre is planned on day 22 (day_number 2), the rest is still in the pool.
const ASSIGNMENTS: AssignmentsMap = {
'22': [{ id: 90, day_id: 22, place_id: 1, order_index: 0, place: { id: 1, name: 'Louvre' } }],
} as unknown as AssignmentsMap
function makePlanner(overrides: Partial<TripPlanner> = {}): TripPlanner {
return buildPlanner({
places: PLACES,
categories: CATEGORIES,
assignments: ASSIGNMENTS,
days: DAYS,
...overrides,
} as Partial<TripPlanner>)
}
function renderBrowser(planner: TripPlanner = makePlanner(), shell: MTripShellApi = buildShell()) {
const view = render(<MPlacesBrowser planner={planner} shell={shell} />)
return { ...view, planner, shell }
}
/** Row button of a place — the whole left side of the row is one button. */
const row = (name: string) => screen.getByRole('button', { name: new RegExp(name) })
describe('MPlacesBrowser', () => {
beforeEach(() => {
resetAllStores()
seedStore(useAddonStore, { addons: [{ id: 'collections', enabled: true }] })
vi.spyOn(collectionsApi, 'list').mockResolvedValue({ collections: [], incomingInvites: [] } as CollectionListResponse)
})
afterEach(() => {
vi.restoreAllMocks()
})
it('FE-MOB-PBROW-001: lists every place with its count divider', () => {
renderBrowser()
expect(screen.getByText('places.count:3')).toBeInTheDocument()
expect(screen.getByText('Louvre')).toBeInTheDocument()
expect(screen.getByText('Rue de Rivoli')).toBeInTheDocument()
// No address → the description takes the sub line.
expect(screen.getByText('Iron lady')).toBeInTheDocument()
})
it('FE-MOB-PBROW-002: badges a planned place with its first day and offers no quick-add', () => {
renderBrowser()
expect(screen.getByText('planner.dayN:2')).toBeInTheDocument()
// Two unplanned places → two quick-add buttons.
expect(screen.getAllByRole('button', { name: 'places.assignToDay' })).toHaveLength(2)
})
it('FE-MOB-PBROW-003: quick-add opens the place-actions sheet with the day picker', () => {
const { shell } = renderBrowser()
fireEvent.click(screen.getAllByRole('button', { name: 'places.assignToDay' })[0])
expect(shell.openSheet).toHaveBeenCalledWith('bract', { placeId: 2, dayPicker: true })
})
it('FE-MOB-PBROW-004: a row tap opens the place-actions sheet without the day picker', () => {
const { shell } = renderBrowser()
fireEvent.click(row('Louvre'))
expect(shell.openSheet).toHaveBeenCalledWith('bract', { placeId: 1, dayPicker: false })
})
it('FE-MOB-PBROW-005: the filter chips write the pool filter to the trip store', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'places.unplanned' }))
expect(useTripStore.getState().placesFilter).toBe('unplanned')
expect(screen.getByText('places.count:2')).toBeInTheDocument()
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'places.planned' }))
expect(useTripStore.getState().placesFilter).toBe('planned')
expect(screen.getByText('places.count:1')).toBeInTheDocument()
expect(screen.getByText('Louvre')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'places.all' }))
expect(useTripStore.getState().placesFilter).toBe('all')
})
it('FE-MOB-PBROW-006: the tracks chip only appears with a track and filters to it', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'places.filterTracks' }))
expect(screen.getByText('places.count:1')).toBeInTheDocument()
expect(screen.getByText('Seine Track')).toBeInTheDocument()
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
})
// The fall back to "all" once the last track is gone lives in useTripPlanner
// (FE-TP-HOOK-177), which stays mounted while this browser is not.
it('FE-MOB-PBROW-007: hides the tracks chip when no place carries a track', () => {
renderBrowser(makePlanner({ places: [LOUVRE, EIFFEL] } as Partial<TripPlanner>))
expect(screen.queryByRole('button', { name: 'places.filterTracks' })).not.toBeInTheDocument()
expect(screen.getByText('places.count:2')).toBeInTheDocument()
})
it('FE-MOB-PBROW-008: entering the browser from the edit segment seeds the unplanned pool', () => {
renderBrowser(makePlanner(), buildShell({ browseFromEdit: true }))
expect(useTripStore.getState().placesFilter).toBe('unplanned')
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-009: the search box matches name and address', () => {
renderBrowser()
const input = screen.getByPlaceholderText('places.search')
fireEvent.change(input, { target: { value: 'rivoli' } })
expect(screen.getByText('Louvre')).toBeInTheDocument()
expect(screen.queryByText('Seine Track')).not.toBeInTheDocument()
fireEvent.change(input, { target: { value: 'seine' } })
expect(screen.getByText('Seine Track')).toBeInTheDocument()
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-010: the category panel toggles the shared filter set and badges its size', () => {
renderBrowser()
const catBtn = screen.getByRole('button', { name: 'places.filters' })
expect(catBtn).toHaveAttribute('aria-expanded', 'false')
fireEvent.click(catBtn)
expect(catBtn).toHaveAttribute('aria-expanded', 'true')
fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
expect([...useTripStore.getState().placesCategoryFilter]).toEqual(['1'])
expect(screen.getByRole('checkbox', { name: 'Sights' })).toHaveAttribute('aria-checked', 'true')
expect(screen.getByText('Louvre')).toBeInTheDocument()
expect(screen.queryByText('Eiffel Tower')).not.toBeInTheDocument()
expect(catBtn).toHaveTextContent('1')
fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
expect(useTripStore.getState().placesCategoryFilter.size).toBe(0)
})
it('FE-MOB-PBROW-032: the panel rating floor narrows the list, badges the button and lands in the store', () => {
const planner = makePlanner({
places: [{ ...LOUVRE, rating_avg: 4.5 }, { ...EIFFEL, rating_avg: 3 }, SEINE],
} as Partial<TripPlanner>)
renderBrowser(planner)
const panelBtn = screen.getByRole('button', { name: 'places.filters' })
fireEvent.click(panelBtn)
const floors = screen.getByRole('group', { name: 'Filter by rating' })
fireEvent.click(within(floors).getByRole('button', { name: '4+' }))
expect(useTripStore.getState().placesRatingFilter).toBe(4)
expect(within(floors).getByRole('button', { name: '4+' })).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByText('Louvre')).toBeInTheDocument()
expect(screen.queryByText('Eiffel Tower')).not.toBeInTheDocument()
expect(screen.queryByText('Seine Track')).not.toBeInTheDocument()
expect(panelBtn).toHaveTextContent('1')
fireEvent.click(within(floors).getByRole('button', { name: 'All' }))
expect(useTripStore.getState().placesRatingFilter).toBe('all')
expect(screen.getByText('Seine Track')).toBeInTheDocument()
})
it('FE-MOB-PBROW-035: the panel badge counts kinds of filter, like the map badge', () => {
renderBrowser()
const panelBtn = screen.getByRole('button', { name: 'places.filters' })
fireEvent.click(panelBtn)
fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Food' }))
// Two categories are one kind of filter.
expect(panelBtn).toHaveTextContent('1')
fireEvent.click(within(screen.getByRole('group', { name: 'Filter by rating' })).getByRole('button', { name: '4+' }))
expect(panelBtn).toHaveTextContent('2')
// The pool has its own chips in plain sight, so the panel badge leaves it out.
fireEvent.click(screen.getByRole('button', { name: 'places.unplanned' }))
expect(panelBtn).toHaveTextContent('2')
})
it('FE-MOB-PBROW-036: a new rating floor starts a fresh selection, as a new pool does', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
fireEvent.click(within(screen.getByRole('group', { name: 'Filter by rating' })).getByRole('button', { name: '1+' }))
expect(screen.queryByText('places.selectionCount:1')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-033: a floor set from the map sheet already thins the list on arrival', () => {
seedStore(useTripStore, { placesRatingFilter: 5 })
renderBrowser(makePlanner({ places: [{ ...LOUVRE, rating_avg: 5 }, EIFFEL] } as Partial<TripPlanner>))
expect(screen.getByText('Louvre')).toBeInTheDocument()
expect(screen.queryByText('Eiffel Tower')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-011: the uncategorized row filters the places without a category', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'No Category' }))
expect([...useTripStore.getState().placesCategoryFilter]).toEqual(['uncategorized'])
expect(screen.getByText('Eiffel Tower')).toBeInTheDocument()
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-012: the uncategorized row is dropped when every place has a category', () => {
const planner = makePlanner({ places: [LOUVRE, SEINE] } as Partial<TripPlanner>)
renderBrowser(planner)
fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
expect(screen.queryByRole('checkbox', { name: 'No Category' })).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Food' })).toBeInTheDocument()
})
it('FE-MOB-PBROW-013: select mode picks rows and reports the count', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
expect(screen.getByText('places.selectionCount:0')).toBeInTheDocument()
fireEvent.click(row('Louvre'))
fireEvent.click(row('Eiffel Tower'))
expect(screen.getByText('places.selectionCount:2')).toBeInTheDocument()
fireEvent.click(row('Louvre'))
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
// Row taps no longer open the actions sheet while selecting.
expect(screen.queryByRole('button', { name: 'places.assignToDay' })).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-014: select all / deselect all covers exactly the visible pool', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(screen.getByRole('button', { name: 'common.selectAll' }))
expect(screen.getByText('places.selectionCount:3')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'common.deselectAll' }))
expect(screen.getByText('places.selectionCount:0')).toBeInTheDocument()
})
it('FE-MOB-PBROW-015: switching the pool filter or searching clears the selection', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(screen.getByRole('button', { name: 'common.selectAll' }))
fireEvent.click(screen.getByRole('button', { name: 'places.unplanned' }))
expect(screen.getByText('places.selectionCount:0')).toBeInTheDocument()
fireEvent.click(row('Eiffel Tower'))
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
fireEvent.change(screen.getByPlaceholderText('places.search'), { target: { value: 'ei' } })
expect(screen.getByText('places.selectionCount:0')).toBeInTheDocument()
})
it('FE-MOB-PBROW-016: leaving select mode drops the selection', () => {
renderBrowser()
const toggle = screen.getByRole('button', { name: 'common.select' })
fireEvent.click(toggle)
fireEvent.click(row('Louvre'))
expect(toggle).toHaveAttribute('aria-pressed', 'true')
fireEvent.click(toggle)
expect(toggle).toHaveAttribute('aria-pressed', 'false')
expect(screen.queryByText(/places.selectionCount/)).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-017: the bulk actions stay disabled until something is selected', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
expect(screen.getByRole('button', { name: 'places.changeCategory' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'inspector.saveToCollection' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'places.deleteSelected' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'common.selectAll' })).toBeEnabled()
})
it('FE-MOB-PBROW-018: the bulk category sheet applies the pick and leaves select mode once it resolved', async () => {
const { planner } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'places.changeCategory' }))
expect(screen.getByRole('dialog', { name: 'Change category' })).toBeInTheDocument()
await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'Food' })) })
expect(planner.confirmChangeCategory).toHaveBeenCalledWith([1], 2)
expect(screen.queryByText(/places.selectionCount/)).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-018b: a failed bulk category change keeps the selection for a retry', async () => {
const planner = makePlanner()
;(planner.confirmChangeCategory as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('nope'))
renderBrowser(planner)
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'places.changeCategory' }))
await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'Food' })) })
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
})
it('FE-MOB-PBROW-019: dismissing the category sheet keeps the selection untouched', async () => {
const { planner } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'places.changeCategory' }))
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Change category' })).not.toBeInTheDocument())
expect(planner.confirmChangeCategory).not.toHaveBeenCalled()
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
})
it('FE-MOB-PBROW-020: the delete confirmation removes the selection in one call', async () => {
const { planner } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(row('Seine Track'))
fireEvent.click(screen.getByRole('button', { name: 'places.deleteSelected' }))
expect(screen.getByText('trip.confirm.deletePlaces:2')).toBeInTheDocument()
await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'common.delete' })) })
expect(planner.confirmDeletePlaces).toHaveBeenCalledWith([1, 3])
expect(screen.queryByText(/places.selectionCount/)).not.toBeInTheDocument()
})
it('routes a Tour row to read-only detail instead of mobile Place bulk management', () => {
const planner = makePlanner({
places: [LOUVRE, TOUR],
isTourPlace: vi.fn((placeId: number) => placeId === TOUR.id),
})
renderBrowser(planner)
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(row('Ridge walk'))
expect(planner.handlePlaceClick).toHaveBeenCalledWith(TOUR.id)
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'places.deleteSelected' })).toBeEnabled()
expect(screen.getByRole('button', { name: 'places.changeCategory' })).toBeEnabled()
expect(screen.getByRole('button', { name: 'common.deselectAll' })).toBeInTheDocument()
})
it('keeps Tour-backed Places out of the Places pool and Tours mode browse/attach only', async () => {
const ridge: TourListItem = {
place_id: TOUR.id, name: 'Ridge walk', tour_type: 'hike', distance: 4, elevation_gain: 100,
elevation_loss: 80, duration: null, difficulty: null, wanderer_ref: null, match_confidence: 1,
max_hiking_difficulty: 2, planned: false, caution: false,
}
seedStore(useAddonStore, { addons: [{ id: 'collections', enabled: true }, { id: 'tours', enabled: true }] })
const list = vi.spyOn(toursApi, 'list').mockResolvedValue({ tours: [ridge] })
const planner = makePlanner({
places: [...PLACES, TOUR],
tours: [ridge],
isTourPlace: vi.fn((placeId: number) => placeId === TOUR.id),
})
const { shell } = renderBrowser(planner)
await waitFor(() => expect(screen.getByText('places.count:3')).toBeInTheDocument())
expect(screen.queryByText('Ridge walk')).not.toBeInTheDocument()
// The planner owns the tour list; the browser does not fetch a second one.
expect(list).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('tab', { name: /^Tours$/i }))
expect(screen.getByRole('option', { name: /Ridge walk/ })).toBeInTheDocument()
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'mobileTrip.importPlaces' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.add' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.select' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: /tours.addToDay|Add to day/i })).toBeInTheDocument()
})
it('FE-MOB-PBROW-020b: a failed bulk delete keeps the selection for a retry', async () => {
const planner = makePlanner()
;(planner.confirmDeletePlaces as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('nope'))
renderBrowser(planner)
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'places.deleteSelected' }))
await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'common.delete' })) })
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
})
it('FE-MOB-PBROW-021: cancelling the delete keeps the selection intact', () => {
const { planner } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'places.deleteSelected' }))
fireEvent.click(screen.getByRole('button', { name: 'common.cancel' }))
expect(planner.confirmDeletePlaces).not.toHaveBeenCalled()
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
})
it('FE-MOB-PBROW-022: the save-to-collection action opens the list picker', async () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(screen.getByRole('button', { name: 'inspector.saveToCollection' }))
await waitFor(() => expect(collectionsApi.list).toHaveBeenCalled())
expect(screen.getByRole('dialog', { name: 'Save 1 to a list' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Save 1 to a list' })).not.toBeInTheDocument())
expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
})
it('FE-MOB-PBROW-023: without the collections addon neither the action nor the sheet exists', () => {
seedStore(useAddonStore, { addons: [] })
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
expect(screen.queryByRole('button', { name: 'inspector.saveToCollection' })).not.toBeInTheDocument()
expect(collectionsApi.list).not.toHaveBeenCalled()
})
it('FE-MOB-PBROW-024: the add button opens a blank place form', () => {
const { planner } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.add' }))
expect(planner.setEditingPlace).toHaveBeenCalledWith(null)
expect(planner.setEditingAssignmentId).toHaveBeenCalledWith(null)
expect(planner.setPrefillCoords).toHaveBeenCalledWith(null)
expect(planner.setShowPlaceForm).toHaveBeenCalledWith(true)
})
it('FE-MOB-PBROW-025: the ellipsis opens the import sheet', () => {
const { shell } = renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'mobileTrip.importPlaces' }))
expect(shell.openSheet).toHaveBeenCalledWith('import')
})
it('FE-MOB-PBROW-026: a read-only member keeps the filters but loses every mutation', () => {
renderBrowser(makePlanner({ can: vi.fn(() => false) } as Partial<TripPlanner>))
expect(screen.getByRole('button', { name: 'places.all' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'mobileTrip.importPlaces' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.select' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.add' })).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-027: the empty unplanned pool celebrates instead of reporting no matches', () => {
renderBrowser(makePlanner({ places: [LOUVRE] } as Partial<TripPlanner>), buildShell({ browseFromEdit: true }))
expect(screen.getByText('places.allPlanned')).toBeInTheDocument()
expect(screen.queryByText('places.noneFound')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-034: an unplanned pool emptied by the rating floor reports no matches, not "all planned"', () => {
seedStore(useTripStore, { placesRatingFilter: 5 })
renderBrowser(makePlanner({ places: [EIFFEL] } as Partial<TripPlanner>), buildShell({ browseFromEdit: true }))
expect(screen.getByText('places.noneFound')).toBeInTheDocument()
expect(screen.queryByText('places.allPlanned')).not.toBeInTheDocument()
})
it('FE-MOB-PBROW-028: a search without hits reports no matches', () => {
renderBrowser()
fireEvent.change(screen.getByPlaceholderText('places.search'), { target: { value: 'zzz' } })
expect(screen.getByText('places.noneFound')).toBeInTheDocument()
expect(screen.getByText('places.count:0')).toBeInTheDocument()
})
it('FE-MOB-PBROW-029: a deleted place drops out of the selection count', async () => {
const shell = buildShell()
const { rerender } = renderBrowser(makePlanner(), shell)
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(screen.getByRole('button', { name: 'common.selectAll' }))
expect(screen.getByText('places.selectionCount:3')).toBeInTheDocument()
await act(async () => {
rerender(<MPlacesBrowser planner={makePlanner({ places: [LOUVRE, EIFFEL] } as Partial<TripPlanner>)} shell={shell} />)
})
expect(screen.getByText('places.selectionCount:2')).toBeInTheDocument()
})
it('FE-MOB-PBROW-031: a markdown description in a row reads as formatted text, not as syntax (#2337)', () => {
const CHARLIE = place({ id: 4, name: 'Checkpoint Charlie', description: '# Checkpoint Charlie\n\nThe **most famous** crossing.' })
renderBrowser(makePlanner({ places: [CHARLIE], assignments: {} } as Partial<TripPlanner>))
expect(screen.getByRole('heading', { name: 'Checkpoint Charlie' })).toBeInTheDocument()
expect(screen.queryByText(/^# Checkpoint/)).toBeNull()
})
it('FE-MOB-PBROW-030: a selection that only matches the visible pool in size is not "all selected"', () => {
renderBrowser()
fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
fireEvent.click(row('Louvre'))
fireEvent.click(row('Eiffel Tower'))
// Narrow to the two categorised places: two visible, two selected — but not
// the same two, so the toolbar must still offer select-all.
fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Food' }))
expect(screen.getByText('places.count:2')).toBeInTheDocument()
expect(screen.getByText('places.selectionCount:2')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.selectAll' })).toBeInTheDocument()
})
})