mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
fix(tours): long rides save, the horse route store stays bounded
A ride sends its full line with heights and every horse route section's own line besides, about fifty bytes a point, so a ride along a few thousand points of bridleway hit the API's 100kb body ceiling and the save failed with a 413. Creating and editing a tour are now measured against TOUR_WRITE_MAX_BYTES (2 MB), like the book and list file routes; every other route, the tour copy among them, keeps 100kb. The horse routes kept in memory are now bounded by section points as well as by count (64 answers, 500k points), so panning a dense map can no longer grow the server by hundreds of megabytes. A search that found no connection can be asked again on the view whose routes are shown, and a ride with a recorded time shows the horseshoe beside its riding time instead of footprints.
This commit is contained in:
@@ -35,6 +35,14 @@ describe('RC-01 Tour day-row facts', () => {
|
||||
expect(screen.getByTestId('plan-tour-icon').querySelector('.lucide-mountain')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('HOR-01: a ride with a recorded time shows the horseshoe beside its riding time', () => {
|
||||
render(<TourDayRowFacts tour={{ ...tour, tour_type: 'horse', max_hiking_difficulty: null }} />);
|
||||
const time = screen.getByTestId('trip-plan-tour-walking-time');
|
||||
expect(time.getAttribute('aria-label')).toMatch(/^Riding time/);
|
||||
expect(time.querySelector('.lucide-horseshoe')).toBeTruthy();
|
||||
expect(time.querySelector('.lucide-footprints')).toBeNull();
|
||||
});
|
||||
|
||||
it.each([false, true])('renders compact facts with accessible meaning (mobile=%s)', (mobile) => {
|
||||
render(<TourDayRowFacts tour={tour} mobile={mobile} />);
|
||||
expect(screen.getByTestId('plan-tour-distance')).toHaveTextContent('4');
|
||||
|
||||
@@ -22,9 +22,9 @@ function knownType(type: string): boolean {
|
||||
return importableTourTypeSchema.safeParse(type).success;
|
||||
}
|
||||
|
||||
/** The icon the time on the move goes by: a ride, a run or a paddle by its type's own, any walk by footprints. */
|
||||
/** The icon the time on the move goes by: a bike tour, a ride on horseback, a run or a paddle by its type's own, any walk by footprints. */
|
||||
function movingIconType(type: string): string {
|
||||
return type === 'kayak' || type === 'bike' || type === 'run' ? type : 'walk';
|
||||
return type === 'kayak' || type === 'bike' || type === 'run' || type === 'horse' ? type : 'walk';
|
||||
}
|
||||
|
||||
export function TourDayRowIcon({ type, size = 27 }: { type: string; size?: number }) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HORSE_ROUTES_ATTRIBUTION, type HorseRoute } from '@trek/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '../../../../tests/helpers/render';
|
||||
import { hikePlannerDefaults } from '../../../../tests/helpers/tourPlannerStub';
|
||||
import { OnlineOnlyError } from '../../../repo/onlineOnly';
|
||||
import { publishTourMapViewport } from '../../../store/tourMapViewportStore';
|
||||
import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
import type { TourPlannerController } from '../planner/useTourPlanner';
|
||||
@@ -17,7 +18,7 @@ vi.mock('../../Map/MapViewAuto', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// FE-HORSE-CARD-001 to FE-HORSE-CARD-007
|
||||
// FE-HORSE-CARD-001 to FE-HORSE-CARD-008
|
||||
|
||||
const VIEW = { south: 47.1, west: 11.2, north: 47.3, east: 11.5 };
|
||||
const ROUTE: HorseRoute = {
|
||||
@@ -181,4 +182,16 @@ describe('HorseRoutesCard (HOR-01)', () => {
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));
|
||||
expect(stub.editWaypoints).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-HORSE-CARD-008: a search without a connection can be asked again on the view found before', async () => {
|
||||
repo.list.mockResolvedValueOnce(reply([ROUTE])).mockRejectedValueOnce(new OnlineOnlyError());
|
||||
render(<HorseRoutesCard planner={planner()} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Find in map view' }));
|
||||
await screen.findByTestId('horse-route-relation/42');
|
||||
act(() => publishTourMapViewport({ ...VIEW, north: 47.31 }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Search this view' }));
|
||||
expect(await screen.findByText('Horse routes need a connection.')).toBeInTheDocument();
|
||||
act(() => publishTourMapViewport(VIEW));
|
||||
expect(screen.getByRole('button', { name: 'Search this view' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ function HorseRoutesBody({ planner }: { planner: TourPlannerController }) {
|
||||
const found = useHorseRoutes(planner.tripId);
|
||||
const [open, setOpen] = useState<HorseRoute | null>(null);
|
||||
const searched = found.searched !== null;
|
||||
const again = searched && (found.stale || found.status === 'failed');
|
||||
const again = searched && (found.stale || found.status === 'failed' || found.status === 'offline');
|
||||
return (
|
||||
<RailCard
|
||||
icon={<Horseshoe size={14} strokeWidth={2} />}
|
||||
|
||||
Reference in New Issue
Block a user