From 6b6d971f0781f04501aec34bb7926fde2566cd57 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 25 Oct 2026 11:03:03 +0100 Subject: [PATCH] fix: the CI findings on the 5.0 release notes and the demo fixes The overview of a trip's tours decides for itself when it shows, which keeps the planner page hook inside its complexity and length limits, and adds nothing to the camera's key while there is no tour to frame. The elevation helper names its empty case for the strict type check, the self-host wording exemption for the retired Dawarich card goes, the day plan test reads the wrapping pills, and the new tests drop their non-null assertions and loops. --- client/scripts/eslint-baseline.json | 2 +- .../Planner/DayPlanSidebar.tours.test.tsx | 4 ++-- .../src/pages/tripPlanner/useTourOverview.test.ts | 13 ++++++++++--- client/src/pages/tripPlanner/useTourOverview.ts | 15 +++++++++++---- .../src/pages/tripPlanner/useTripPlannerPage.ts | 6 +----- client/src/utils/routeGeometry.test.ts | 6 +++--- .../unit/nest/budget.foreign-custom-split.test.ts | 14 ++++++++++---- shared/scripts/i18n-selfhost-wording.mjs | 6 ------ shared/src/tours/tours.metrics.ts | 3 ++- 9 files changed, 40 insertions(+), 29 deletions(-) diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index 492f6dc91..e02a8db56 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -1,6 +1,6 @@ { "src": { - "@typescript-eslint/no-non-null-assertion": 384, + "@typescript-eslint/no-non-null-assertion": 382, "@typescript-eslint/no-misused-promises": 849, "no-console": 88, "complexity": 221, diff --git a/client/src/components/Planner/DayPlanSidebar.tours.test.tsx b/client/src/components/Planner/DayPlanSidebar.tours.test.tsx index 2618560d1..042028f66 100644 --- a/client/src/components/Planner/DayPlanSidebar.tours.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tours.test.tsx @@ -201,9 +201,9 @@ describe('DayPlanSidebar', () => { const view = render(); const tourPreview = view.container.querySelector('[data-testid="trip-plan-tour-description"]') as HTMLElement; expect(tourPreview).toHaveTextContent(description); - // One line like the row's name, so hover controls make the description give way instead of wrapping. + // The description keeps to one line; the pills beside it wrap rather than run out of the row. expect(tourPreview).toHaveStyle({ whiteSpace: 'nowrap', maxHeight: '1.3em' }); - expect(tourPreview.parentElement).toHaveClass('flex-nowrap', 'overflow-hidden'); + expect(tourPreview.parentElement).toHaveClass('flex-wrap', 'overflow-hidden'); expect(screen.getByTestId('trip-plan-tour-walking-time')).toHaveTextContent('Walk: 1 h'); expect(screen.getByTestId('trip-plan-tour-breaks')).toHaveTextContent('Breaks: 35 min'); expect(screen.getByTestId('trip-plan-tour-planned-duration')).toHaveTextContent('Planned: 1 h 35 min'); diff --git a/client/src/pages/tripPlanner/useTourOverview.test.ts b/client/src/pages/tripPlanner/useTourOverview.test.ts index f9eaa9896..9cd2b0b5c 100644 --- a/client/src/pages/tripPlanner/useTourOverview.test.ts +++ b/client/src/pages/tripPlanner/useTourOverview.test.ts @@ -6,6 +6,13 @@ import { resetAllStores } from '../../../tests/helpers/store'; import type { Place } from '../../types'; import { useTourOverview } from './useTourOverview'; +const OPEN = { + activeTab: 'tour-planner', + tourPlanner: { mode: { type: 'neutral' } }, + wandererBrowser: { open: false }, +} as const; +const EDITING = { ...OPEN, tourPlanner: { mode: { type: 'new-draft' } } } as const; + const PLACES = [ { id: 1, lat: 46.6, lng: 12.1, route_geometry: '[[46.5,12.0],[46.7,12.2],[46.6,12.3]]' }, { id: 2, lat: 45.4, lng: 12.3, route_geometry: '[[45.43,12.32],[45.44,12.34]]' }, @@ -18,12 +25,12 @@ describe('useTourOverview', () => { beforeEach(() => resetAllStores()); it('FE-TP-TOUROVERVIEW-001: shows the tours with a line, not the other places', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, true)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, OPEN)); expect(result.current.tourPlaces.map((place) => place.id)).toEqual([1, 2]); }); it('FE-TP-TOUROVERVIEW-002: frames each line by its corners', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, true)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, OPEN)); expect(result.current.framePoints).toEqual([ [46.5, 12.0], [46.7, 12.3], @@ -34,7 +41,7 @@ describe('useTourOverview', () => { }); it('FE-TP-TOUROVERVIEW-003: shows nothing while a tour is open', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, false)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, EDITING)); expect(result.current).toEqual({ tourPlaces: [], framePoints: [], focusSuffix: '' }); }); }); diff --git a/client/src/pages/tripPlanner/useTourOverview.ts b/client/src/pages/tripPlanner/useTourOverview.ts index 95048355f..0960d3bd1 100644 --- a/client/src/pages/tripPlanner/useTourOverview.ts +++ b/client/src/pages/tripPlanner/useTourOverview.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { TourPlannerController } from '../../components/Tours/planner/useTourPlanner'; import { useTourMapVisibility } from '../../components/Tours/useTourMapVisibility'; import type { Place } from '../../types'; import { parseRenderableRouteGeometry } from '../../utils/routeGeometry'; @@ -7,15 +8,21 @@ import { parseRenderableRouteGeometry } from '../../utils/routeGeometry'; * The trip's tours on the Tours map while none is open, as the plan map shows them: * each tour's place, whose line and marker the map draws in the tour's colour, and the * corners of those lines for the camera to frame. A tour hidden from the map in the - * list stays off it here too. Empty while `showing` is off (a tour open, an import - * viewed, the wanderer browser up), so the map then shows that alone. + * list stays off it here too. Empty off the Tours tab and while a tour is open, an + * import viewed or the wanderer browser up, so the map then shows that alone. */ export function useTourOverview( tripId: number, places: readonly Place[], tourPlaceIds: ReadonlySet, - showing: boolean + view: { + activeTab: string; + tourPlanner: Pick; + wandererBrowser: { open: boolean }; + } ) { + const showing = + view.activeTab === 'tour-planner' && view.tourPlanner.mode.type === 'neutral' && !view.wandererBrowser.open; const { hiddenIds } = useTourMapVisibility(tripId, tourPlaceIds); const tourPlaces = useMemo( () => @@ -29,7 +36,7 @@ export function useTourOverview( tourPlaces, framePoints, // Part of the camera's key, so the map frames the tours again once they are loaded or one is shown or hidden. - focusSuffix: showing ? `:tours:${tourPlaces.map((place) => place.id).join(',')}` : '', + focusSuffix: tourPlaces.length ? `:tours:${tourPlaces.map((place) => place.id).join(',')}` : '', }; } diff --git a/client/src/pages/tripPlanner/useTripPlannerPage.ts b/client/src/pages/tripPlanner/useTripPlannerPage.ts index 099ac2910..d6c6b6156 100644 --- a/client/src/pages/tripPlanner/useTripPlannerPage.ts +++ b/client/src/pages/tripPlanner/useTripPlannerPage.ts @@ -42,11 +42,7 @@ export function useTripPlannerPage() { useAssistantEntityLinks({ planner, workspace }) const tourDetailOpenerRef = useRef<{ placeId: number; element: HTMLElement } | null>(null) - // With no tour open, the Tours map shows the trip's tours and frames them. - const tourOverview = useTourOverview( - tripId, places, planner.tourPlaceIds, - activeTab === 'tour-planner' && tourPlanner.mode.type === 'neutral' && !wandererBrowser.open, - ) + const tourOverview = useTourOverview(tripId, places, planner.tourPlaceIds, { activeTab, tourPlanner, wandererBrowser }) const { focusPoints, lastPlanMapViewportRef } = usePlannerFocusPoints(activeTab, places, tourPlanner, wandererBrowser, tourOverview.framePoints) // PAD-06: an obstacle or area the paddle checklist asks for moves the camera as a picked search result does. diff --git a/client/src/utils/routeGeometry.test.ts b/client/src/utils/routeGeometry.test.ts index e7ab19c26..fb1961ae7 100644 --- a/client/src/utils/routeGeometry.test.ts +++ b/client/src/utils/routeGeometry.test.ts @@ -31,9 +31,9 @@ describe('analyzeRouteGeometry', () => { const analysis = analyzeRouteGeometry(JSON.stringify([ [45.44, 12.32, 2], [45.441, 12.32, 9], [45.442, 12.32, 1], [45.443, 12.32, 4], [45.444, 12.32, 3], ])) - expect(analysis!.maxEle).toBe(9) - expect(analysis!.gain).toBe(7) - expect(analysis!.loss).toBe(8) + expect(analysis?.maxEle).toBe(9) + expect(analysis?.gain).toBe(7) + expect(analysis?.loss).toBe(8) }) it('maps route endpoints and interpolates focus between prepared samples', () => { diff --git a/server/tests/unit/nest/budget.foreign-custom-split.test.ts b/server/tests/unit/nest/budget.foreign-custom-split.test.ts index 41f7ac872..3ed87aebb 100644 --- a/server/tests/unit/nest/budget.foreign-custom-split.test.ts +++ b/server/tests/unit/nest/budget.foreign-custom-split.test.ts @@ -32,10 +32,16 @@ afterAll(async () => { }); async function seedQuarters() { - const users = []; - for (let i = 0; i < 4; i++) users.push((await makeUser(orm())).user); + const users = [ + (await makeUser(orm())).user, + (await makeUser(orm())).user, + (await makeUser(orm())).user, + (await makeUser(orm())).user, + ]; const trip = await makeTrip(orm(), users[0].id); - for (const u of users.slice(1)) await addTripMember(orm(), trip.id, u.id); + await addTripMember(orm(), trip.id, users[1].id); + await addTripMember(orm(), trip.id, users[2].id); + await addTripMember(orm(), trip.id, users[3].id); await budget.createBudgetItem(trip.id, { name: 'Sky Lagoon', currency: 'ISK', @@ -54,7 +60,7 @@ describe('a custom split in a foreign currency', () => { const cents = result.balances.map((b) => Math.round(b.balance * 100)); expect(cents.reduce((a, c) => a + c, 0)).toBe(0); // The payer has the lowest id, so the tie hands them one of the three rounded-up quarters. - expect(result.balances.find((b) => b.user_id === users[0].id)!.balance).toBe(748.13); + expect(result.balances.find((b) => b.user_id === users[0].id)?.balance).toBe(748.13); const owed = result.flows .filter((f) => f.to.user_id === users[0].id) .reduce((a, f) => a + Math.round(f.amount * 100), 0); diff --git a/shared/scripts/i18n-selfhost-wording.mjs b/shared/scripts/i18n-selfhost-wording.mjs index 91342eda1..1d882ae03 100644 --- a/shared/scripts/i18n-selfhost-wording.mjs +++ b/shared/scripts/i18n-selfhost-wording.mjs @@ -35,12 +35,6 @@ export const ALLOWED = [ 'settings.about.descriptionManaged instead, so neither reader is told the ' + 'wrong thing and self-hosters lose nothing.', }, - { - key: 'system_notice.release_notes.feature_dawarich_body', - because: - 'it calls Dawarich self-hosted, not TREK, which is what Dawarich is, and the ' + - 'release notice carrying it is shown only where the managed condition is false', - }, { key: 'system_notice.v3_thankyou.body', because: diff --git a/shared/src/tours/tours.metrics.ts b/shared/src/tours/tours.metrics.ts index 89fbbdb4b..c14e130d3 100644 --- a/shared/src/tours/tours.metrics.ts +++ b/shared/src/tours/tours.metrics.ts @@ -32,7 +32,8 @@ const ELEVATION_NOISE_M = 5; export function elevationGainLoss(heights: readonly number[]): { gain: number; loss: number } { let gain = 0; let loss = 0; - let ref = heights[0]; + let [ref] = heights; + if (ref === undefined) return { gain, loss }; for (const height of heights) { const delta = height - ref; if (delta >= ELEVATION_NOISE_M) gain += delta;