diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index 937ce8279..4cc7018ee 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -1,10 +1,10 @@ { "src": { "@typescript-eslint/no-non-null-assertion": 375, - "@typescript-eslint/no-misused-promises": 842, + "@typescript-eslint/no-misused-promises": 840, "no-console": 88, "complexity": 218, - "@typescript-eslint/no-explicit-any": 144, + "@typescript-eslint/no-explicit-any": 143, "trek/no-swallowed-catch": 128, "max-params": 31, "trek/store-without-selector": 67, @@ -12,7 +12,7 @@ "react-hooks/set-state-in-effect": 192, "@typescript-eslint/no-floating-promises": 163, "react-hooks/exhaustive-deps": 114, - "@typescript-eslint/no-unused-vars": 123, + "@typescript-eslint/no-unused-vars": 122, "react-hooks/immutability": 28, "max-lines-per-function": 130, "react-hooks/static-components": 50, @@ -35,10 +35,10 @@ }, "tests": { "@typescript-eslint/no-explicit-any": 489, - "@typescript-eslint/no-non-null-assertion": 1911, + "@typescript-eslint/no-non-null-assertion": 1907, "@typescript-eslint/no-misused-promises": 22, "@typescript-eslint/no-unused-vars": 51, - "@typescript-eslint/no-floating-promises": 33, + "@typescript-eslint/no-floating-promises": 27, "trek/no-window-globals": 325, "trek/no-swallowed-catch": 14, "react-hooks/globals": 5, diff --git a/client/scripts/rtl-baseline.json b/client/scripts/rtl-baseline.json index b692cfc21..4af5b800d 100644 --- a/client/scripts/rtl-baseline.json +++ b/client/scripts/rtl-baseline.json @@ -63,7 +63,7 @@ "components/Vacay/VacayCalendar.tsx": 3, "components/Vacay/VacayMonthCard.tsx": 7, "components/Vacay/VacaySettings.tsx": 1, - "components/shared/ContextMenu.tsx": 3, + "components/shared/ContextMenu.tsx": 1, "components/shared/CustomDateTimePicker.tsx": 2, "components/shared/CustomSelect.tsx": 4, "components/shared/CustomTimePicker.tsx": 3, diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index 978a06062..5b589778c 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -3,7 +3,7 @@ "src/api/client.ts": 1873, "src/components/Admin/AdminPluginsPanel.test.tsx": 2704, "src/components/Admin/AdminPluginsPanel.tsx": 1266, - "src/components/Budget/CostsPanel.test.tsx": 2730, + "src/components/Budget/CostsPanel.test.tsx": 2725, "src/components/Budget/CostsPanel.tsx": 1607, "src/components/Map/MapView.tsx": 1200, "src/components/Map/MapViewGL.test.tsx": 2919, @@ -12,7 +12,7 @@ "src/components/Planner/DayPlanSidebar.test.tsx": 5349, "src/components/Planner/DayPlanSidebar.tsx": 2910, "src/components/Planner/PlaceFormModal.test.tsx": 2836, - "src/components/Planner/PlaceInspector.tsx": 1129, + "src/components/Planner/PlaceInspector.tsx": 1128, "src/components/Studio/TravelElements.tsx": 1559, "src/components/Studio/countryShapes.ts": 2595, "src/index.css": 1636, @@ -22,6 +22,6 @@ "src/pages/TripPlannerPage.tsx": 1128, "src/pages/atlas/useAtlas.ts": 1207, "src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518, - "src/pages/tripPlanner/useTripPlanner.test.tsx": 3270, + "src/pages/tripPlanner/useTripPlanner.test.tsx": 3269, "src/styles/studio.css": 1918 } diff --git a/client/scripts/theme-baseline.json b/client/scripts/theme-baseline.json index af357029a..5061ad14f 100644 --- a/client/scripts/theme-baseline.json +++ b/client/scripts/theme-baseline.json @@ -110,7 +110,7 @@ "components/SystemNotices/SystemNoticeModal.tsx": 107, "components/Todo/TodoListPanel.tsx": 6, "components/Todo/todoListModel.ts": 3, - "components/Trips/TripFormModal.tsx": 9, + "components/Trips/TripFormModal.tsx": 6, "components/Vacay/VacayBadge.tsx": 3, "components/Vacay/VacayCalendar.tsx": 20, "components/Vacay/VacayMonthCard.tsx": 5, @@ -250,7 +250,7 @@ "mobile/screens/trip/sheets/MPlaceSheet.tsx": 23, "mobile/screens/trip/sheets/MReservationSheet.tsx": 2, "mobile/screens/trip/sheets/MTransportFormSheet.tsx": 9, - "mobile/screens/trip/sheets/MTransportSheet.tsx": 12, + "mobile/screens/trip/sheets/MTransportSheet.tsx": 9, "mobile/screens/trip/sheets/MTripSheetUi.tsx": 5, "mobile/screens/trip/sheets/PlCategoryPicker.tsx": 2, "mobile/screens/trip/sheets/PlFileAttach.tsx": 7, diff --git a/client/src/components/Budget/CostsCategoryBreakdown.tsx b/client/src/components/Budget/CostsCategoryBreakdown.tsx index 359f466cd..2d106cba4 100644 --- a/client/src/components/Budget/CostsCategoryBreakdown.tsx +++ b/client/src/components/Budget/CostsCategoryBreakdown.tsx @@ -99,3 +99,26 @@ export function CostsCategoryBreakdown({ ); } + +/** + * What the totals leave out, beside the total (#2819), and the way back: a click + * counts every category again. The table view has no breakdown to undo it in. + */ +export function LeftOutChip({ + label, + onCountAll, + className, +}: { + label: string; + onCountAll: () => void; + className: string; +}) { + const { t } = useTranslation(); + return ( + + + + ); +} diff --git a/client/src/components/Budget/CostsPanel.test.tsx b/client/src/components/Budget/CostsPanel.test.tsx index 4967e250e..aff01472f 100644 --- a/client/src/components/Budget/CostsPanel.test.tsx +++ b/client/src/components/Budget/CostsPanel.test.tsx @@ -1780,20 +1780,15 @@ describe('CostsPanel — remaining paths', () => { expect(screen.getAllByText('XBT').length).toBeGreaterThanOrEqual(2) }) - it('FE-W5COSTS-049: a solo trip defaults the payment counterpart to myself', async () => { - const user = userEvent.setup() + it('FE-W5COSTS-049: a solo trip offers no payment, there is nobody to pay (#2689)', async () => { server.use( http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })), http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), ) render() - await user.click(await screen.findByRole('button', { name: 'Add payment' })) - await user.type(await screen.findByPlaceholderText('0,00'), '10') - - // From and To both resolve to me, so the transfer stays unsavable. - const submits = screen.getAllByRole('button', { name: 'Add payment' }) - expect(submits[submits.length - 1]).toBeDisabled() + expect(await screen.findByRole('button', { name: 'Add expense' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Add payment' })).not.toBeInTheDocument() }) it('FE-W5COSTS-050: the expense modal shows uploaded avatars and rejects over-precise input', async () => { diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index 0c52841de..c23fdbee1 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -1,5 +1,5 @@ import { Fragment, useState, useMemo, useCallback, useId, type ReactNode } from 'react' -import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, ArrowLeftRight, Check, RotateCcw, Pencil, Trash2, AlertCircle, Download, StickyNote, ChevronDown, Receipt, ScanLine, Users } from 'lucide-react' +import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, ArrowLeftRight, Check, RotateCcw, Pencil, Trash2, AlertCircle, Download, ChevronDown, Receipt, ScanLine, Users } from 'lucide-react' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useToast } from '../shared/Toast' @@ -31,7 +31,7 @@ import { useExpenseDeleteConfirm } from './useExpenseDeleteConfirm' import { FlowPills } from './FlowPills' import { CostsSplitChips } from './CostsSplitChips' import { CostsModeCard } from './CostsModeCard' -import { CostsCategoryBreakdown } from './CostsCategoryBreakdown' +import { CostsCategoryBreakdown, LeftOutChip } from './CostsCategoryBreakdown' import { CostsCurrencyPicker } from './CostsCurrencyPicker' import type { BudgetParticipantFinal, ReceiptLine } from '@trek/shared' import type { BudgetItem, BudgetItemReceipt } from '../../types' @@ -243,12 +243,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps {t('costs.yourShare')}{fmt0(totals.myShare)} {t('costs.youPaid')}{fmt0(totals.myPaid)} : {t('costs.expensesCount', { count: countedCount })}} - {/* Also the way back where the breakdown is not on show (the table view). */} - {leftOutLabel && ( - - - - )}} /> + {leftOutLabel && }} /> ) // Spend by category; a click leaves one out of the totals for a look without it (#2819). @@ -1063,7 +1058,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo const payerSection = ( label={t('costs.whoPaid')} @@ -1137,7 +1132,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo const splitSection = ( label={t('costs.split') || 'Split'} diff --git a/client/src/components/Budget/SettlementModal.tsx b/client/src/components/Budget/SettlementModal.tsx index 85ff8ae6c..6026386c2 100644 --- a/client/src/components/Budget/SettlementModal.tsx +++ b/client/src/components/Budget/SettlementModal.tsx @@ -105,7 +105,7 @@ export function SettlementModal({ {t('common.cancel')} - + void save()} disabled={!valid || saving}> {editing ? t('common.save') : t('costs.addPayment')} diff --git a/client/src/components/Budget/useCostsMode.test.ts b/client/src/components/Budget/useCostsMode.test.ts index de6577900..276a086b4 100644 --- a/client/src/components/Budget/useCostsMode.test.ts +++ b/client/src/components/Budget/useCostsMode.test.ts @@ -1,14 +1,16 @@ // FE-BUDGET-COSTSMODE-001 to FE-BUDGET-COSTSMODE-003 import { act, renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { buildTrip } from '../../../tests/helpers/factories'; import { resetAllStores } from '../../../tests/helpers/store'; import { useTripStore, type TripStoreState } from '../../store/tripStore'; import { useCostsMode } from './useCostsMode'; +const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn() })); +vi.mock('../shared/Toast', () => ({ useToast: () => toast })); + let updateTrip: ReturnType; -let addToast: ReturnType; const trip = (costsMode?: 'split' | 'totals') => buildTrip({ id: 7, ...(costsMode ? { costs_mode: costsMode } : {}) }); @@ -16,12 +18,7 @@ beforeEach(() => { resetAllStores(); updateTrip = vi.fn(async () => buildTrip({ id: 7 })); useTripStore.setState({ updateTrip } as unknown as Partial); - addToast = vi.fn(); - (window as unknown as { __addToast: unknown }).__addToast = addToast; -}); - -afterEach(() => { - delete (window as unknown as { __addToast?: unknown }).__addToast; + toast.error.mockClear(); }); describe('useCostsMode (#1714)', () => { @@ -50,7 +47,7 @@ describe('useCostsMode (#1714)', () => { await result.current.setSplitting(true); }); expect(updateTrip).toHaveBeenLastCalledWith(7, { costs_mode: 'split' }); - expect(addToast.mock.calls.map((c) => c[1])).toContain('error'); + expect(toast.error).toHaveBeenCalled(); expect(result.current.saving).toBe(false); }); }); diff --git a/client/src/components/Collections/CollectionFilterBar.test.tsx b/client/src/components/Collections/CollectionFilterBar.test.tsx index 3a12bcbf2..f7f7d42a9 100644 --- a/client/src/components/Collections/CollectionFilterBar.test.tsx +++ b/client/src/components/Collections/CollectionFilterBar.test.tsx @@ -24,7 +24,7 @@ const CATEGORY_OPTIONS: CategoryOption[] = [ function makeProps(overrides: Partial = {}): HarnessProps { return { statusFilter: 'all' as StatusFilter, - counts: { all: 3, idea: 1, want: 1, visited: 1 }, + counts: { all: 3, idea: 1, want: 1, visited: 1, revisit: 0 }, categoryFilter: 'all', categoryOptions: CATEGORY_OPTIONS, ratingFilter: 'all', diff --git a/client/src/components/Journey/JourneyMap.test.tsx b/client/src/components/Journey/JourneyMap.test.tsx index 019391519..71977096c 100644 --- a/client/src/components/Journey/JourneyMap.test.tsx +++ b/client/src/components/Journey/JourneyMap.test.tsx @@ -683,8 +683,8 @@ describe('JourneyMap', () => { const { rerender } = render(); const vector = await waitFor(() => { const layer = lastGlLayer(); - expect(layer).toBeTruthy(); - return layer!; + if (!layer) throw new Error('no GL layer yet'); + return layer; }); rerender(); diff --git a/client/src/components/Journey/JourneyMap.tsx b/client/src/components/Journey/JourneyMap.tsx index 4dd2c03cc..dc85f653b 100644 --- a/client/src/components/Journey/JourneyMap.tsx +++ b/client/src/components/Journey/JourneyMap.tsx @@ -307,7 +307,7 @@ function fitOptions(paddingBottom: number | undefined, paddingStart = 0): L.FitB */ function rasterBase(url: string, satellite: boolean, gcj: boolean): L.TileLayer { const tileUrl = satellite ? (gcj ? AMAP_SATELLITE : SATELLITE_TILE_URL) : url - return L.tileLayer(tileUrl, { + const options: L.TileLayerOptions & { referrerPolicy: ReferrerPolicy } = { maxZoom: satellite ? SATELLITE_TILE_MAXZOOM : 18, attribution: attributionForTile(tileUrl), referrerPolicy: 'strict-origin-when-cross-origin', @@ -317,7 +317,65 @@ function rasterBase(url: string, satellite: boolean, gcj: boolean): L.TileLayer // updates and keep a larger ring of off-screen tiles ready. updateWhenIdle: false, keepBuffer: 4, - } as any) + } + return L.tileLayer(tileUrl, options) +} + +interface BaseRefs { + mapRef: RefObject + tileUrlRef: RefObject + tileLayerRef: RefObject + glLayerRef: RefObject +} + +/** + * The map's base and the reader's switch between it and the satellite imagery + * (#2594). The map effect draws it through drawBase; the switch redraws it in + * place, since a rebuild would drop every marker and track for a new backdrop. + */ +function useJourneyBase(satellite: boolean, { mapRef, tileUrlRef, tileLayerRef, glLayerRef }: BaseRefs) { + const satelliteRef = useRef(satellite) + useLayoutEffect(() => { + satelliteRef.current = satellite + }) + // Bumped by every base drawn, so a vector style still loading for a base the + // reader has already switched away from never lands on the map. + const baseDrawRef = useRef(0) + // Which base the map carries now, so the switch redraws only on a change. + const drawnSatelliteRef = useRef(satellite) + + /** + * Draws the base: the satellite imagery while the reader asked for it, + * otherwise the basemap, which is a vector style unless the user brought their + * own raster template. Whatever drew it before is taken off first. A vector + * style is a GL canvas of its own, so leaving it under the imagery would keep + * a WebGL context busy drawing what nobody can see. + */ + const drawBase = useCallback((map: L.Map, torn: () => boolean) => { + const draw = ++baseDrawRef.current + drawnSatelliteRef.current = satelliteRef.current + tileLayerRef.current?.remove() + tileLayerRef.current = null + detachBasemapLayer(glLayerRef.current) + glLayerRef.current = null + const url = tileUrlRef.current + if (!satelliteRef.current && isVectorStyle(url)) { + void attachVectorBasemap(map, url, glLayerRef, () => torn() || draw !== baseDrawRef.current) + return + } + const tiles = rasterBase(url, satelliteRef.current, !isVectorStyle(url) && isGcj02Basemap(url)) + tiles.addTo(map) + tileLayerRef.current = tiles + }, [tileUrlRef, tileLayerRef, glLayerRef]) + + // Nothing to do when the map effect has just drawn the base the switch asks for. + useEffect(() => { + const map = mapRef.current + if (!map || drawnSatelliteRef.current === satellite) return + drawBase(map, () => mapRef.current !== map) + }, [satellite, drawBase, mapRef]) + + return { drawBase, satelliteRef } } function JourneyMap( @@ -350,18 +408,9 @@ function JourneyMap( const tileLayerRef = useRef(null) // GL layer or the raster stand-in a browser without WebGL gets instead (#2288). const glLayerRef = useRef(null) - // Read by the map effect like the tile url, and switched in place by its own - // effect below: a rebuild would drop every marker and track for a new backdrop. - const satellite = baseLayer === 'satellite' - const satelliteRef = useRef(satellite) - satelliteRef.current = satellite - // Bumped by every base drawn, so a vector style still loading for a base the - // reader has already switched away from never lands on the map. - const baseDrawRef = useRef(0) - // Which base the map carries now, so the switch effect redraws only on a change. - const drawnSatelliteRef = useRef(satellite) const containerRef = useRef(null) const mapRef = useRef(null) + const { drawBase, satelliteRef } = useJourneyBase(baseLayer === 'satellite', { mapRef, tileUrlRef, tileLayerRef, glLayerRef }) const markersRef = useRef>(new Map()) const itemsRef = useRef([]) const highlightedRef = useRef(null) @@ -392,29 +441,6 @@ function JourneyMap( plannedRef.current = plannedEntryIds }) - /** - * Draws the base: the satellite imagery while the reader asked for it, - * otherwise the basemap, which is a vector style unless the user brought their - * own raster template. Whatever drew it before is taken off first. A vector - * style is a GL canvas of its own, so leaving it under the imagery would keep - * a WebGL context busy drawing what nobody can see. - */ - const drawBase = useCallback((map: L.Map, torn: () => boolean) => { - const draw = ++baseDrawRef.current - drawnSatelliteRef.current = satelliteRef.current - tileLayerRef.current?.remove() - tileLayerRef.current = null - detachBasemapLayer(glLayerRef.current) - glLayerRef.current = null - const url = tileUrlRef.current - if (!satelliteRef.current && isVectorStyle(url)) { - void attachVectorBasemap(map, url, glLayerRef, () => torn() || draw !== baseDrawRef.current) - return - } - const tiles = rasterBase(url, satelliteRef.current, !isVectorStyle(url) && isGcj02Basemap(url)) - tiles.addTo(map) - tileLayerRef.current = tiles - }, []) const syncEntryCard = useEntryCardSync({ onCardClickRef: onMarkerCardClickRef, mapRef, markersRef, itemsRef, highlightedRef }) @@ -643,15 +669,7 @@ function JourneyMap( if (satelliteRef.current) return if (isVectorStyle(tileUrl)) restyleBasemap(glLayerRef.current, tileUrl) else tileLayerRef.current?.setUrl(tileUrl) - }, [tileUrl]) - - // The reader's base layer switch (#2594). Nothing to do when the map effect - // has just drawn the base the switch asks for. - useEffect(() => { - const map = mapRef.current - if (!map || drawnSatelliteRef.current === satellite) return - drawBase(map, () => mapRef.current !== map) - }, [satellite, drawBase]) + }, [tileUrl, satelliteRef]) // Photo layer (#1614). Its own effect on purpose: photos arriving must not tear // down and rebuild the map the way the entry effect does. Redrawn after every diff --git a/client/src/components/Journey/JourneyMapGL.tsx b/client/src/components/Journey/JourneyMapGL.tsx index 42a86127c..b1a76f931 100644 --- a/client/src/components/Journey/JourneyMapGL.tsx +++ b/client/src/components/Journey/JourneyMapGL.tsx @@ -268,7 +268,10 @@ function JourneyMapGL( // Read when a rebuilt map loads, and switched in place by its own effect below. const satellite = baseLayer === 'satellite' const satelliteRef = useRef(satellite) - satelliteRef.current = satellite + // Before the map effects below, like the refs above. + useEffect(() => { + satelliteRef.current = satellite + }) const mapLangRef = useRef(mapLang) mapLangRef.current = mapLang diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index 44c6d3148..89cd7a014 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -1429,19 +1429,30 @@ describe('downloadTripPDF — transport notes', () => { // ── Large text (#2817) ────────────────────────────────────────────────────── +/** The preview frame, which every export below opens. */ +function previewFrame(): HTMLIFrameElement { + const frame = getIframe() + if (!frame) throw new Error('no preview frame') + return frame +} + describe('downloadTripPDF — large text', () => { beforeEach(() => { localStorage.removeItem('trek_pdf_large_text') }) - const largeToggle = () => document.querySelector('#pdf-large-text-toggle') + const largeToggle = () => { + const toggle = document.querySelector('#pdf-large-text-toggle') + if (!toggle) throw new Error('no large text switch') + return toggle + } it('FE-PDF-LARGE-001: the document keeps its compact type by default and offers the switch off', async () => { await downloadTripPDF(richArgs) - const html = getIframe()!.srcdoc + const html = previewFrame().srcdoc expect(html).not.toContain('pdf-large-text"') - expect(largeToggle()!.getAttribute('aria-checked')).toBe('false') + expect(largeToggle().getAttribute('aria-checked')).toBe('false') // One scale and one set of greys, at today's values unless the class is on. expect(html).toContain('--pdf-text: 1;') expect(html).toContain('body.pdf-large-text {') @@ -1451,7 +1462,7 @@ describe('downloadTripPDF — large text', () => { it('FE-PDF-LARGE-002: the clock and the booking code of a transport scale with the rest', async () => { await downloadTripPDF(richArgs) - const html = getIframe()!.srcdoc + const html = previewFrame().srcdoc expect(html).toContain('') expect(html).toContain('
Code: ABC123
') @@ -1460,11 +1471,11 @@ describe('downloadTripPDF — large text', () => { it('FE-PDF-LARGE-003: turning it on enlarges the open preview at once and is remembered', async () => { await downloadTripPDF(richArgs) - largeToggle()!.click() + largeToggle().click() - expect(largeToggle()!.getAttribute('aria-checked')).toBe('true') + expect(largeToggle().getAttribute('aria-checked')).toBe('true') expect(localStorage.getItem('trek_pdf_large_text')).toBe('1') - const body = getIframe()!.contentDocument?.body + const body = previewFrame().contentDocument?.body if (body) expect(body.classList.contains('pdf-large-text')).toBe(true) }) @@ -1473,8 +1484,8 @@ describe('downloadTripPDF — large text', () => { localStorage.setItem('trek_pdf_page_break_per_day', '0') await downloadTripPDF(richArgs) - expect(getIframe()!.srcdoc).toContain('') - expect(largeToggle()!.getAttribute('aria-checked')).toBe('true') + expect(previewFrame().srcdoc).toContain('') + expect(largeToggle().getAttribute('aria-checked')).toBe('true') localStorage.removeItem('trek_pdf_page_break_per_day') }) @@ -1482,8 +1493,8 @@ describe('downloadTripPDF — large text', () => { vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('blocked') }) await downloadTripPDF(richArgs) - expect(getIframe()!.srcdoc).not.toContain('pdf-large-text"') - expect(largeToggle()!.getAttribute('aria-checked')).toBe('false') + expect(previewFrame().srcdoc).not.toContain('pdf-large-text"') + expect(largeToggle().getAttribute('aria-checked')).toBe('false') }) }) @@ -1512,12 +1523,12 @@ describe('downloadTripPDF — photos at thumbnail size', () => { const withImage = (image_url: string) => ({ ...richArgs, - assignments: { '10': [{ ...assignmentForDay, place: { ...placeWithDetails, image_url } }] } as any, + assignments: { '10': [{ ...assignmentForDay, place: { ...placeWithDetails, image_url } }] } as unknown as typeof richArgs.assignments, }) it('FE-PDF-THUMB-001: an uploaded photo is baked in as a small JPEG instead of its full-size URL', async () => { await downloadTripPDF(withImage('/uploads/colosseum.jpg')) - const html = getIframe()!.srcdoc + const html = previewFrame().srcdoc expect(html).toContain(``) expect(html).not.toContain('colosseum.jpg') @@ -1526,7 +1537,7 @@ describe('downloadTripPDF — photos at thumbnail size', () => { it('FE-PDF-THUMB-002: a photo from another origin keeps its URL', async () => { await downloadTripPDF(withImage('https://images.example.com/colosseum.jpg')) - expect(getIframe()!.srcdoc).toContain('') + expect(previewFrame().srcdoc).toContain('') }) it('FE-PDF-THUMB-003: a closed export dialog stops the photos and opens nothing', async () => { diff --git a/client/src/components/Packing/usePackingCategorySharing.test.ts b/client/src/components/Packing/usePackingCategorySharing.test.ts index b78baddee..70ffba6c9 100644 --- a/client/src/components/Packing/usePackingCategorySharing.test.ts +++ b/client/src/components/Packing/usePackingCategorySharing.test.ts @@ -18,14 +18,18 @@ const theirs = buildPackingItem({ id: 3, name: 'Lamp', category: 'Camp', owner_i const socks = buildPackingItem({ id: 4, name: 'Socks', category: 'Clothes', owner_id: ME }); const diary = buildPackingItem({ id: 5, name: 'Diary', category: 'Camp', owner_id: ME, is_private: 1 }); -function setup(items: PackingItem[], setPackingItemSharing: Mock = vi.fn(() => Promise.resolve())) { +function setup( + items: PackingItem[], + setPackingItemSharing: Mock = vi.fn(() => Promise.resolve()), + view: 'common' | 'personal' = 'common' +) { const toast = { success: vi.fn() }; const t = vi.fn((key: string) => key); const hook = renderHook(() => usePackingCategorySharing({ tripId: 7, items, - view: 'common', + view, defaultCategory: 'Other', currentUserId: ME, setPackingItemSharing, @@ -99,7 +103,8 @@ describe('usePackingCategorySharing', () => { is_private: 1, recipients: recipients(2, 3), }); - const { result, setPackingItemSharing, toast } = setup([kettle, pans]); + // Shared items are private ones with people added, so they live in the personal view. + const { result, setPackingItemSharing, toast } = setup([kettle, pans], undefined, 'personal'); act(() => result.current.openFor('Food')); expect(result.current.visibility).toBe('shared'); expect(result.current.recipientIds).toEqual([2, 3]); diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index 7d49f4aa9..e341bffc7 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -252,14 +252,10 @@ export default function PlaceInspector({ const saveVersion = useSaveToCollectionStore(s => s.version) const [savedInCollection, setSavedInCollection] = useState(false) const [hoursExpanded, setHoursExpanded] = useState(false) - // Folded for the place on show only: any other place selected, coming back - // included, opens in full. - const [collapsed, setCollapsed] = useState(false) - const [foldedFor, setFoldedFor] = useState(place.id) - if (foldedFor !== place.id) { - setFoldedFor(place.id) - setCollapsed(false) - } + // Folded for the place on show only; any other selected, coming back included, opens in full. + const [foldedId, setFoldedId] = useState(null) + if (foldedId !== null && foldedId !== place?.id) setFoldedId(null) + const collapsed = foldedId === place?.id const [editingName, setEditingName] = useState(false) const [nameValue, setNameValue] = useState('') const nameInputRef = useRef(null) @@ -389,7 +385,7 @@ export default function PlaceInspector({ editingName={editingName} nameInputRef={nameInputRef} nameValue={nameValue} setNameValue={setNameValue} commitNameEdit={commitNameEdit} handleNameKeyDown={handleNameKeyDown} startNameEdit={startNameEdit} photoCredit={} onUpdatePlace={onUpdatePlace} collapsed={collapsed} - onToggleCollapsed={collapsible ? () => setCollapsed(c => !c) : undefined} + onToggleCollapsed={collapsible ? () => setFoldedId(collapsed ? null : place.id) : undefined} onClose={onClose} /> {/* The body scrolls inside the capped card; each block keeps its natural height (#1195). */} @@ -481,8 +477,7 @@ export default function PlaceInspector({ {/* Footer: the day action and the ways out on the left, the "…" and edit on the right. */} - {/* Measured as a container: squeezed between the sidebars (a tablet) the - worded buttons drop their words before the row would wrap. */} + {/* A container: squeezed between the sidebars, the buttons drop their words before wrapping. */}