From bff9bcde92c3c41d88812a97d2ceff67174cc54f Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 12:52:13 +0200 Subject: [PATCH] fix(tours): read ways past the grade cap as not asked instead of untagged A long hike or mountain bike tour can run over more distinct ways than one grade request may ask about. The ways past that cap were never asked, yet the facets read them as carrying no surface, way type or grade in OpenStreetMap. They now land under 'Could not be asked this time' with the unanswered ones. --- .../Tours/bike/useRouteWays.test.tsx | 24 +++++++++++++++++++ .../src/components/Tours/bike/useRouteWays.ts | 17 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/client/src/components/Tours/bike/useRouteWays.test.tsx b/client/src/components/Tours/bike/useRouteWays.test.tsx index b17ad573e..12caacb8b 100644 --- a/client/src/components/Tours/bike/useRouteWays.test.tsx +++ b/client/src/components/Tours/bike/useRouteWays.test.tsx @@ -1,3 +1,4 @@ +import { TOUR_TRAIL_GRADE_MAX_WAYS } from '@trek/shared'; import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { encodePolyline } from '../../../../tests/helpers/polyline'; @@ -175,4 +176,27 @@ describe('useRouteWays (BIK-01 to BIK-05, RUN-02)', () => { rerender(input({ matching: 'bike', withGrades: true })); expect(result.current.status).toBe('loading'); }); + + it('FE-TOUR-WAYS-HOOK-010: ways past the cap were never asked, so they read as unasked, not as untagged (MAP-04)', async () => { + const count = TOUR_TRAIL_GRADE_MAX_WAYS + 2; + const edges = Array.from({ length: count }, (_, index) => ({ + begin_shape_index: 0, + end_shape_index: 1, + length: 0.01, + use: 'path', + way_id: index + 1, + })); + trace.valhallaTraceAttributes.mockResolvedValue({ ...ANSWER, edges }); + gradesRepo.grades.mockResolvedValue({ ways: [], unanswered: [5] }); + const { result } = renderHook(() => useRouteWays(input({ matching: 'foot', withGrades: true }))); + await settle(); + const asked = gradesRepo.grades.mock.calls[0][1].way_ids as number[]; + expect(asked).toHaveLength(TOUR_TRAIL_GRADE_MAX_WAYS); + const unasked = result.current.grades?.unasked; + expect(unasked?.has(5)).toBe(true); + expect(unasked?.has(count - 1)).toBe(true); + expect(unasked?.has(count)).toBe(true); + expect(unasked?.has(1)).toBe(false); + expect(unasked?.size).toBe(3); + }); }); diff --git a/client/src/components/Tours/bike/useRouteWays.ts b/client/src/components/Tours/bike/useRouteWays.ts index 2db89a432..a123e544b 100644 --- a/client/src/components/Tours/bike/useRouteWays.ts +++ b/client/src/components/Tours/bike/useRouteWays.ts @@ -44,6 +44,20 @@ interface Question { withGrades: boolean; } +/** + * The ways whose grades stay unknown after an answer: those it left unanswered, + * and those past the cap that were never asked, so neither reads as a way + * OpenStreetMap records nothing for. + */ +function unaskedWays(ways: RouteWays, asked: readonly number[], unanswered: readonly number[]): Set { + const askedIds = new Set(asked); + const unasked = new Set(unanswered); + for (const edge of ways.edges) { + if (edge.wayId !== null && !askedIds.has(edge.wayId)) unasked.add(edge.wayId); + } + return unasked; +} + /** The ways of a route, and where asked for their grades; the grades' outcome as it came. */ async function askRouteWays( { tripId, route, matching, withGrades }: Question, @@ -59,7 +73,8 @@ async function askRouteWays( try { const answer = await tourTrailGradeRepo.grades(tripId, { way_ids: ids }, signal); const byWay = new Map(answer.ways.map((grade) => [grade.id, grade])); - return { status: 'ready', ways, grades: { status: 'ready', byWay, unasked: new Set(answer.unanswered) } }; + const unasked = unaskedWays(ways, ids, answer.unanswered); + return { status: 'ready', ways, grades: { status: 'ready', byWay, unasked } }; } catch (failure) { const status = failure instanceof OnlineOnlyError ? 'offline' : 'failed'; return { status: 'ready', ways, grades: { status, ...NO_GRADES } };