fix(tours): fit the merged tracks together after integration

Types, test expectations and helpers the seven tracks each changed on their
own, brought in line with the integrated code. tours.service.ts and the tours
repository grew past the size limit with every track and hand their row and
write helpers to modules of their own.
This commit is contained in:
Maurice
2026-10-20 16:30:55 +02:00
parent 65ee472d66
commit f4750352e6
56 changed files with 582 additions and 505 deletions
@@ -50,7 +50,7 @@ import { markdownLinkComponents } from '../shared/markdownLink';
import { TourDayRowAvatar } from './TourDayRowFacts';
import { TourGpxExportButton } from './TourGpxExportButton';
import TourActivitySection from './activity/TourActivitySection';
import type { PlanValues } from './activity/tourActivityCompare';
import type { PlanValues } from './activity/planComparison';
import { TourSaveToCollectionButton } from './collection/TourSaveToCollectionButton';
import { PaddleCheckSection } from './paddle/PaddleCheckSection';
import { TourPackingSection } from './packing/TourPackingSection';
@@ -7,7 +7,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
import type { TourActivity } from '@trek/shared';
import { describe, expect, it, vi } from 'vitest';
import TourActivityCompare, { formatDifference } from './TourActivityCompare';
import type { PlanValues } from './tourActivityCompare';
import type { PlanValues } from './planComparison';
const plan: PlanValues = {
distanceKm: 2,
@@ -18,7 +18,7 @@ import {
type CompareKey,
type CompareRow,
type PlanValues,
} from './tourActivityCompare';
} from './planComparison';
type Translate = (key: string, params?: Record<string, unknown>) => string;
@@ -167,7 +167,9 @@ function deviationFields(
unit: DistanceUnit,
t: Translate
): TourMetricField[] {
const deviation = planDeviation(activity.geometry, plan.line);
// Latitude and longitude only: how far the track strayed is measured on the map, not in height.
const recorded = activity.geometry.map(([lat, lng]) => [lat, lng]);
const deviation = planDeviation(recorded, plan.line);
const offLabel = t('tours.activity.compare.offPlan');
if (!deviation) return [{ label: offLabel, value: t('tours.activity.compare.noLine') }];
const offInfo = {
@@ -10,7 +10,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useTourActivityCompareStore } from '../../../store/tourActivityCompareStore';
import type { Day } from '../../../types';
import TourActivitySection from './TourActivitySection';
import type { PlanValues } from './tourActivityCompare';
import type { PlanValues } from './planComparison';
import type { TourActivities } from './useTourActivities';
const hook = vi.hoisted(() => ({ state: null as unknown }));
@@ -116,7 +116,7 @@ describe('TourActivitySection (ACT-01)', () => {
expect(current.askRemove).toHaveBeenCalledWith(current.activities?.[0]);
hook.state = state({ assigning: {}, removing: activity() });
rerender(<TourActivitySection tripId={1} tour={tour} days={days} assignments={assignments} canEdit />);
rerender(<TourActivitySection tripId={1} tour={tour} days={days} assignments={assignments} canEdit plan={plan} planColor="plan-paint" />);
expect(screen.getByTestId('tour-activity-dialog')).toBeInTheDocument();
expect(screen.getByText('tours.activity.removeTitle')).toBeInTheDocument();
});
@@ -16,7 +16,7 @@ import { CardAction, TourNotice } from '../tourParts';
import TourActivityCompare from './TourActivityCompare';
import TourActivityDialog from './TourActivityDialog';
import { ACTUAL_TRACK_COLOR } from './activityCompareParts';
import type { PlanValues } from './tourActivityCompare';
import type { PlanValues } from './planComparison';
import { analysisOfLine, executionOf, executionsOf, recordedDay, type TourExecution } from './tourActivityModel';
import { useActivityComparison, type ActivityComparisonState } from './useActivityComparison';
import { useTourActivities, type TourActivities } from './useTourActivities';
@@ -4,7 +4,7 @@
// how far the recorded line strayed from the planned one.
import type { TourActivityMetrics } from '@trek/shared';
import { describe, expect, it } from 'vitest';
import { compareRows, MEASURED_POINTS, OFF_PLAN_M, planDeviation, type PlanValues } from './tourActivityCompare';
import { compareRows, MEASURED_POINTS, OFF_PLAN_M, planDeviation, type PlanValues } from './planComparison';
const plan: PlanValues = {
distanceKm: 8,
@@ -15,6 +15,7 @@ const edge = (from: number, wayId: number | null, lengthM = 100): RouteWayEdge =
lengthM,
surface: 'paved',
way: 'path',
sac: 'none',
wayId,
});
@@ -17,6 +17,7 @@ const edge = (from: number, to: number, wayId = 1): RouteWayEdge => ({
lengthM: 100,
surface: 'unknown',
way: 'path',
sac: 'none',
wayId,
});
@@ -33,6 +33,7 @@ const edge = (from: number, wayId: number): RouteWayEdge => ({
// The router's own reading, which no facet may take for a recorded value.
surface: 'paved',
way: 'path',
sac: 'none',
wayId,
});
const WAYS: RouteWays = { line: ROUTE, edges: [edge(0, 1), edge(1, 2), edge(2, 3)] };
@@ -73,6 +73,11 @@ beforeEach(() => {
seedStore(useSettingsStore, { settings: { distance_unit: 'metric' } });
});
/** The last of the elements found: the card rendered most recently. */
function lastOf<T>(items: T[]): T | undefined {
return items[items.length - 1];
}
describe('LegVariantsCard', () => {
it('FE-TOUR-LEG-VARIANTS-CARD-001: names the section by its ends and lists each way with its figures', () => {
renderCard(comparison());
@@ -129,9 +134,9 @@ describe('LegVariantsCard', () => {
expect(actions.apply).toHaveBeenCalledTimes(1);
renderCard(comparison({ selected: 'current' }));
expect(screen.getAllByTestId('leg-variants-apply').at(-1)).toBeDisabled();
expect(lastOf(screen.getAllByTestId('leg-variants-apply'))).toBeDisabled();
renderCard(comparison({ selected: 'a' }), { isSaving: true });
expect(screen.getAllByTestId('leg-variants-apply').at(-1)).toBeDisabled();
expect(lastOf(screen.getAllByTestId('leg-variants-apply'))).toBeDisabled();
});
it('FE-TOUR-LEG-VARIANTS-CARD-005: while a choice is checked the list stands still and the button says what happens', async () => {
@@ -146,15 +151,15 @@ describe('LegVariantsCard', () => {
renderCard(comparison({ phase: 'loading', variants: [way('current')] }));
expect(screen.getByRole('status')).toHaveTextContent('Looking for other ways');
renderCard(comparison({ phase: 'none', variants: [way('current')] }));
expect(screen.getAllByRole('status').at(-1)).toHaveTextContent('The router knows no other way');
expect(lastOf(screen.getAllByRole('status'))).toHaveTextContent('The router knows no other way');
renderCard(comparison({ phase: 'failed', variants: [way('current')] }));
expect(screen.getAllByRole('status').at(-1)).toHaveTextContent('The router did not answer');
expect(lastOf(screen.getAllByRole('status'))).toHaveTextContent('The router did not answer');
renderCard(comparison({ notice: 'notHeld' }));
expect(screen.getAllByRole('status').at(-1)).toHaveTextContent('The router does not keep to this way');
expect(lastOf(screen.getAllByRole('status'))).toHaveTextContent('The router does not keep to this way');
renderCard(comparison({ notice: 'stale' }));
expect(screen.getAllByRole('status').at(-1)).toHaveTextContent('The tour changed in the meantime');
expect(lastOf(screen.getAllByRole('status'))).toHaveTextContent('The tour changed in the meantime');
renderCard(comparison({ notice: 'failed' }));
expect(screen.getAllByRole('status').at(-1)).toHaveTextContent('This way could not be checked');
expect(lastOf(screen.getAllByRole('status'))).toHaveTextContent('This way could not be checked');
});
it('FE-TOUR-LEG-VARIANTS-CARD-007: the overlay shows only while a section is compared; cancel, the cross and Escape close it', async () => {
@@ -1,7 +1,7 @@
// FE-TOUR-ROUND-HIKE-CARD-001 to FE-TOUR-ROUND-HIKE-CARD-009: the card over the planner map that
// looks for round hikes out of a stop and takes one on purpose (RTE-03).
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, within } from '../../../../tests/helpers/render';
import { seedStore } from '../../../../tests/helpers/store';
import { idleRoundHikes } from '../../../../tests/helpers/tourPlannerStub';
@@ -116,7 +116,7 @@ describe('RoundHikeCard', () => {
expect(screen.getByLabelText('Distance')).toHaveValue('6.2');
expect(screen.getByText('mi')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('round-hike-find'));
expect(actions.find.mock.calls[0][0]).toBeCloseTo(9_978, 0);
expect(vi.mocked(actions.find).mock.calls[0][0]).toBeCloseTo(9_978, 0);
});
it('FE-TOUR-ROUND-HIKE-CARD-004: while it looks, the bar fills, the rounds found show and nothing can be taken yet', async () => {
@@ -88,7 +88,7 @@ describe('a run wherever a tour shows (RUN-01)', () => {
expect(screen.queryByTestId('tour-detail-run-profile')).toBeNull();
});
it('FE-TOUR-RUN-MARK-007: a trail run goes by its own profile, and its time and pace read as any run's (RUN-02)', () => {
it("FE-TOUR-RUN-MARK-007: a trail run goes by its own profile, and its time and pace read as any run's (RUN-02)", () => {
const trail = { ...run, run_profile: 'trail' as const, run_pace_seconds_per_km: 450, duration: 60 };
render(<RunHeadPills tour={trail} className="head-pill" />);
expect(screen.getByTestId('tour-detail-run-profile')).toHaveTextContent('Trail running');
@@ -13,7 +13,7 @@ export interface SavedTourStage extends TourStage {
export function savedStagePoints(tour: TourListItem, saved: readonly TourWaypoint[]): TourPlannerWaypoint[] {
let next = 0;
const points = plannerWaypointsOf(saved, () => `saved-${next++}`);
return normalizePlannerWaypoints(points, openedTourFields(tour).tourType === 'kayak');
return normalizePlannerWaypoints(points, openedTourFields(tour).tourType);
}
/**
+1 -1
View File
@@ -8,7 +8,7 @@ import { focusDialog, trapTab } from './dialogFocus'
import { useDiscardGuard } from './useDiscardGuard'
/** A font size that follows the user's text size setting for its tier. */
export const fs = (px: number, tier: 'caption' | 'body' | 'subtitle' = 'caption'): CSSProperties => ({ fontSize: `calc(${px}px * var(--fs-scale-${tier}, 1))` })
export const fs = (px: number, tier: 'caption' | 'body' | 'subtitle' | 'title' = 'caption'): CSSProperties => ({ fontSize: `calc(${px}px * var(--fs-scale-${tier}, 1))` })
const EYEBROW = 'font-geist font-bold uppercase tracking-[.08em] text-content-faint'
const BACKDROP = 'trek-modal-backdrop trek-backdrop-enter fixed inset-0 z-[10000] flex justify-center bg-[rgba(15,23,42,0.5)] px-4' // theme-lint-disable: the dim every shared/Modal draws