mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
761ec4e66f
An imported GPX track was a line drawn beside the day: the map showed the scenic route while the day kept driving the motorway, so the distance, the drive time and every warning belonged to a road nobody was taking. Norway publishes its scenic routes that way, and so does every touring club worth importing from. A track becomes a route the day takes by refinement, not by conversion. Route through what we have, find the point of the track the road misses by the most, make that point a via, go again. Each round roughly halves the worst miss, because the new via lands in the middle of what the last one left unfollowed, so a scenic route usually fits inside ten vias — and the loop stops when the road is close enough rather than after a fixed count. Every point of the track would route exactly and take a hundred requests; two would route in seconds and follow nothing. The day is routed whole per round, not leg by leg: it is a single request already, and per-leg would multiply every round by the number of legs against a host that answers one request a second. The track is oriented to the day first, because half of Norway's routes are published southbound, and the legs it covers are cleared before the new chain lands, so applying a track twice does not stack. Offered from the day's own badges, and only once something has been imported. Tinted while the day carries vias: the rail draws none of them, so that badge is the only place a drive shaped by hand differs from one the router picked.
125 lines
5.6 KiB
TypeScript
125 lines
5.6 KiB
TypeScript
import React from 'react'
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
import { TranslationProvider } from '../../i18n'
|
|
import RoadtripTrackModal from './RoadtripTrackModal'
|
|
import type { FollowTrack, TrackChoice } from './useFollowTrack'
|
|
|
|
const wrap = (ui: React.ReactElement) => render(<TranslationProvider>{ui}</TranslationProvider>)
|
|
|
|
const track = (over: Partial<TrackChoice> & { id: number; name: string }): TrackChoice => ({
|
|
color: '#0ea5e9',
|
|
points: [{ lat: 52, lng: 13 }, { lat: 52, lng: 14 }],
|
|
lengthKm: 68,
|
|
gapKm: 0.4,
|
|
...over,
|
|
})
|
|
|
|
function follow(over: Partial<FollowTrack> = {}): FollowTrack {
|
|
return {
|
|
dayId: 7,
|
|
open: vi.fn(),
|
|
close: vi.fn(),
|
|
tracks: [track({ id: 3, name: 'Atlantic Road' })],
|
|
busy: false,
|
|
round: 0,
|
|
error: null,
|
|
outcome: null,
|
|
apply: vi.fn().mockResolvedValue(undefined),
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
|
viaCount: 0,
|
|
available: true,
|
|
...over,
|
|
}
|
|
}
|
|
|
|
describe('RoadtripTrackModal', () => {
|
|
it('FE-TRACKMODAL-001: closed is nothing at all, not a hidden dialog', () => {
|
|
const { container } = wrap(<RoadtripTrackModal follow={follow({ dayId: null })} dayNumber={1} />)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('FE-TRACKMODAL-002: a trip with no tracks says where tracks come from', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ tracks: [] })} dayNumber={2} />)
|
|
expect(screen.getByText('No tracks in this trip')).toBeTruthy()
|
|
expect(screen.getByText(/GPX or KML/)).toBeTruthy()
|
|
})
|
|
|
|
it('FE-TRACKMODAL-003: a track close to the day says so rather than quoting a distance', () => {
|
|
wrap(<RoadtripTrackModal follow={follow()} dayNumber={1} />)
|
|
expect(screen.getByText(/along this day/)).toBeTruthy()
|
|
expect(screen.getByText(/68 km long/)).toBeTruthy()
|
|
})
|
|
|
|
it('FE-TRACKMODAL-004: a track that runs elsewhere is offered with the distance to here', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ tracks: [track({ id: 9, name: 'Elsewhere', gapKm: 240 })] })} dayNumber={1} />)
|
|
expect(screen.getByText(/240 km off this day/)).toBeTruthy()
|
|
})
|
|
|
|
it('FE-TRACKMODAL-005: nothing is applied until a track is picked', () => {
|
|
const state = follow()
|
|
wrap(<RoadtripTrackModal follow={state} dayNumber={1} />)
|
|
|
|
const action = screen.getByRole('button', { name: 'Follow this track' })
|
|
expect((action as HTMLButtonElement).disabled).toBe(true)
|
|
|
|
fireEvent.click(screen.getByText('Atlantic Road'))
|
|
expect((screen.getByRole('button', { name: 'Follow this track' }) as HTMLButtonElement).disabled).toBe(false)
|
|
// What it will do, before it does it: a track becomes a dozen waypoints on the day.
|
|
expect(screen.getByText(/via points/)).toBeTruthy()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Follow this track' }))
|
|
expect(state.apply).toHaveBeenCalledWith(3)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-006: a picked track can be unpicked', () => {
|
|
wrap(<RoadtripTrackModal follow={follow()} dayNumber={1} />)
|
|
fireEvent.click(screen.getByText('Atlantic Road'))
|
|
fireEvent.click(screen.getByText('Atlantic Road'))
|
|
expect((screen.getByRole('button', { name: 'Follow this track' }) as HTMLButtonElement).disabled).toBe(true)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-007: a run says which round it is on rather than going blank', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ busy: true, round: 4 })} dayNumber={1} />)
|
|
expect(screen.getByRole('status').textContent).toMatch(/round 4/)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-008: a finished run reports how close the drive got', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ outcome: { vias: 6, strayKm: 0.8, capped: false } })} dayNumber={1} />)
|
|
expect(screen.getByRole('status').textContent).toMatch(/6 via points placed/)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-009: a run that ran out of vias admits the gap it left', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ outcome: { vias: 12, strayKm: 2.4, capped: true } })} dayNumber={1} />)
|
|
// The honest form: it did what it could, and the number says how much that was worth.
|
|
expect(screen.getByRole('status').textContent).toMatch(/no closer than/)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-010: a drive that already followed the track is not dressed up as work', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ outcome: { vias: 0, strayKm: 0, capped: false } })} dayNumber={1} />)
|
|
expect(screen.getByRole('status').textContent).toMatch(/already followed/)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-011: a failure is an alert, not a silent nothing', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ error: 'route' })} dayNumber={1} />)
|
|
expect(screen.getByRole('alert').textContent).toMatch(/routing service/)
|
|
})
|
|
|
|
it('FE-TRACKMODAL-012: dropping the day\'s vias is offered only once it has some', () => {
|
|
const state = follow({ viaCount: 5 })
|
|
const { rerender } = wrap(<RoadtripTrackModal follow={state} dayNumber={1} />)
|
|
const drop = screen.getByRole('button', { name: /Drop 5 via points/ })
|
|
fireEvent.click(drop)
|
|
expect(state.clear).toHaveBeenCalled()
|
|
|
|
rerender(<TranslationProvider><RoadtripTrackModal follow={follow({ viaCount: 0 })} dayNumber={1} /></TranslationProvider>)
|
|
expect(screen.queryByRole('button', { name: /Drop/ })).toBeNull()
|
|
})
|
|
|
|
it('FE-TRACKMODAL-013: while it runs, nothing else in the dialog can be touched', () => {
|
|
wrap(<RoadtripTrackModal follow={follow({ busy: true, viaCount: 2 })} dayNumber={1} />)
|
|
expect((screen.getByRole('button', { name: /Atlantic Road/ }) as HTMLButtonElement).disabled).toBe(true)
|
|
expect((screen.getByRole('button', { name: /Drop 2 via points/ }) as HTMLButtonElement).disabled).toBe(true)
|
|
})
|
|
})
|