Files
TREK/client/src/pages/CollectionsPage.test.tsx
T
Maurice 0644106f7a fix(collections): a marker click shows the place, every time
A marker click toggled the selection, so the second click on the marker of the
place that was open closed it again, which read as the details refusing to
open: every other click on the same marker showed nothing. A marker click now
selects and never clears; the map background and the close control do that,
and a list row still toggles as before.

Closes #2431
2026-09-21 19:35:41 +02:00

170 lines
8.2 KiB
TypeScript

// Regression coverage for issue #1485 — the "Add place" button vanished once a
// collection held its first mappable place on a wide/desktop layout. The button
// now lives inside the filter row (delegated via `canAddPlace`) for a populated
// list, and as an empty-state CTA when the list has no places yet. These tests
// render the page against a mocked useCollections hook (per the Page pattern) and
// assert the Add affordance stays reachable — and that there is exactly one of it.
import { describe, it, expect, vi } from 'vitest'
import React from 'react'
import { render, screen } from '../../tests/helpers/render'
import CollectionsPage from './CollectionsPage'
// Stub every child so the test isolates CollectionsPage's own toolbar/branch JSX
// (the Add button is rendered by the page itself, not by any child). This also
// keeps the Leaflet/Mapbox map out of jsdom.
vi.mock('../components/Layout/Navbar', () => ({ default: () => <nav /> }))
vi.mock('../components/Collections/ListsRail', () => ({ default: () => <div /> }))
vi.mock('../components/Collections/ListEditorModal', () => ({ default: () => null }))
vi.mock('../components/Collections/CollectionHero', () => ({ default: () => <div /> }))
vi.mock('../components/Collections/CollectionList', () => ({ default: () => <div data-testid="list" /> }))
// The filter row hosts the Add button for a populated list; mirror that contract
// so the page-level "reachable + exactly one" assertion still holds end to end.
vi.mock('../components/Collections/CollectionFilterBar', () => ({
default: (props: { canAddPlace?: boolean; onAddPlace?: () => void }) =>
props.canAddPlace
? <button type="button" onClick={props.onAddPlace}>collections.addPlace</button>
: <div />,
}))
const mapPanelProps = vi.hoisted(() => ({ current: null as null | { onSelect: (id: number) => void; onDeselect: () => void } }))
vi.mock('../components/Collections/CollectionMapPanel', () => ({
default: (props: { onSelect: (id: number) => void; onDeselect: () => void }) => {
mapPanelProps.current = props
return <div data-testid="map" />
},
}))
vi.mock('../components/Collections/CopyToTripModal', () => ({ default: () => null }))
vi.mock('../components/Collections/MoveToListModal', () => ({ default: () => null }))
vi.mock('../components/Collections/ShareCollectionModal', () => ({ default: () => null }))
vi.mock('../components/Collections/AddPlaceToCollectionModal', () => ({ default: () => null }))
vi.mock('../components/Collections/CollectionPlaceDetail', () => ({ default: () => null }))
vi.mock('../components/Collections/LabelManager', () => ({ default: () => null }))
vi.mock('../components/Collections/BulkAssignLabelModal', () => ({ default: () => null }))
vi.mock('../components/shared/Modal', () => ({ default: () => null }))
const mockUseCollections = vi.fn()
vi.mock('./collections/useCollections', () => ({
useCollections: () => mockUseCollections(),
}))
// A complete-enough hook return; individual tests override the fields that matter.
function makeHook(overrides: Record<string, unknown> = {}) {
const noop = vi.fn()
return {
t: (k: string) => k,
language: 'en',
dark: false,
navigate: noop,
isWide: true,
heroRef: { current: null },
heroHeight: 0,
listColRef: { current: null },
listColRect: null,
categories: [],
collections: [{ id: 1, name: 'Test', is_owner: true }],
ownedLists: [{ id: 1, name: 'Test', is_owner: true }],
sharedLists: [],
activeCollection: { id: 1, name: 'Test', is_owner: true, color: '#6366f1' },
isAllSaved: false,
isOwner: true,
myRole: 'owner',
canEdit: true,
canDelete: true,
canShare: true,
shareMemberCount: 0,
activeId: 1,
places: [{ id: 10 }],
visiblePlaces: [{ id: 10 }],
mappable: [{ id: 10 }],
hasMappable: true,
members: [],
incomingInvites: [],
counts: { all: 1, idea: 0, want: 1, visited: 0 },
view: 'list',
statusFilter: 'all',
categoryFilter: 'all',
categoryOptions: [],
search: '',
selectedPlaceId: null,
selectMode: false,
selectedIds: [],
labels: [],
labelFilter: [],
labelOptions: [],
loading: false,
placesLoading: false,
setView: noop, setStatusFilter: noop, setCategoryFilter: noop, setLabelFilter: noop,
setSearch: noop, setSelectedPlaceId: noop, setSelectMode: noop, toggleSelect: noop,
updatePlace: noop,
showLabelManager: false, setShowLabelManager: noop, labelPickerOpen: false, setLabelPickerOpen: noop,
handleCreateLabel: noop, handleUpdateLabel: noop, handleDeleteLabel: noop, handleBulkAssignLabels: noop, handleAssignPlaceLabels: noop,
editorTarget: null, setEditorTarget: noop, handleEditorCreated: noop,
showAddPlace: false, setShowAddPlace: noop, handlePlaceAdded: noop,
confirmDeleteList: null, setConfirmDeleteList: noop,
mobileRailOpen: false, setMobileRailOpen: noop,
showShare: false, setShowShare: noop, handleAfterLeave: noop,
selectedPlace: null, detailPlace: null, detailCategories: [], handleCloseDetail: noop,
handleDetailStatus: noop, handleDetailRemove: noop,
copyIds: null, openCopyForSelectedPlace: noop, openCopyForSelection: noop, closeCopy: noop, handleCopyToTrip: noop,
handleSelectList: noop, handleDeleteList: noop,
handleStatusChange: noop, handleDeletePlace: noop, handleDeleteSelected: noop,
handleAcceptInvite: noop, handleDeclineInvite: noop,
allVisibleSelected: false, handleSelectAll: noop,
listPickerMode: null, setListPickerMode: noop, handleMoveToList: noop, handleDuplicateToList: noop,
...overrides,
}
}
describe('CollectionsPage — Add place button reachability (#1485)', () => {
it('keeps the Add button visible on a wide desktop once the list has a mappable place', () => {
// The exact bug: wide layout + one place with coordinates + editable list.
mockUseCollections.mockReturnValue(makeHook())
render(<CollectionsPage />)
const addButtons = screen.getAllByRole('button', { name: 'collections.addPlace' })
// Present, and exactly one — no duplicate affordance alongside it.
expect(addButtons).toHaveLength(1)
})
it('does not show the Add button for a viewer who cannot edit', () => {
mockUseCollections.mockReturnValue(makeHook({ canEdit: false, isOwner: false, myRole: 'viewer' }))
render(<CollectionsPage />)
expect(screen.queryByRole('button', { name: 'collections.addPlace' })).toBeNull()
})
it('shows the Add button on an empty collection', () => {
mockUseCollections.mockReturnValue(makeHook({ places: [], visiblePlaces: [], mappable: [], hasMappable: false }))
render(<CollectionsPage />)
expect(screen.getAllByRole('button', { name: 'collections.addPlace' })).toHaveLength(1)
})
})
describe('CollectionsPage — the map survives a filter that matches nothing', () => {
it('keeps the map when a label filter empties the list', () => {
// Clicking a label with no places behind it left visiblePlaces empty, and
// the layout hung off exactly that: the map was torn out and the page
// reflowed to full width. A filter should empty the map, not remove it.
mockUseCollections.mockReturnValue(makeHook({ visiblePlaces: [], mappable: [], hasMappable: true }))
render(<CollectionsPage />)
expect(screen.getByTestId('map')).toBeInTheDocument()
})
it('still hides the map for a list with nothing mappable in it at all', () => {
mockUseCollections.mockReturnValue(makeHook({ mappable: [], hasMappable: false }))
render(<CollectionsPage />)
expect(screen.queryByTestId('map')).toBeNull()
})
})
describe('CollectionsPage: a marker click shows the place (#2431)', () => {
it('selects the place a marker stands for, even when that place is already selected', () => {
const setSelectedPlaceId = vi.fn()
mockUseCollections.mockReturnValue(makeHook({ selectedPlaceId: 10, setSelectedPlaceId }))
render(<CollectionsPage />)
// A second click on the marker of the open place used to toggle it away, which
// read as the details refusing to open. The map background is what clears.
mapPanelProps.current!.onSelect(10)
expect(setSelectedPlaceId).toHaveBeenLastCalledWith(10)
mapPanelProps.current!.onDeselect()
expect(setSelectedPlaceId).toHaveBeenLastCalledWith(null)
})
})