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 } };