mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
test(roadtrip): cover the surface the domain shipped without
The domain arrived with 557 lines, a service test, and nothing else. No controller test, no e2e, and no MCP test — so the guard chain, the trip scoping on every route, the cross-trip track check and the day_edit parity were all unasserted, and the whole domain sat in the 80 catch-all where it could shed twenty points against the headroom of the domains beside it. The e2e is the one that earns its place. `@RequireAddon` is metadata and does nothing on its own, and AddonGuard is not global, so the ordering the controller's own comment calls load-bearing is exactly the kind of thing a later change removes without noticing — it had already happened once. Deleting the guard now turns three cases red, which I checked by deleting it. The MCP tools had never executed at all: the shared harness seeds the addon disabled, so `when: roadtripAddonOn` kept all five from attaching. They now run against the same checks the REST routes make, including the demo account and a member without day_edit, because the repo requires the two surfaces to move together. Two client modules that had no test get one: the overlay builder, where two decisions live that are not cosmetic — which offered road is drawn as the one you are on, and that a label hangs where its own route is unlike every other, rather than at a midpoint that can fall on a stretch two routes share — and the service marker, where the thing worth pinning is that an ordinary place gets nothing back, and that the anchor arithmetic includes the border. The domain carries its own coverage floor now instead of hiding in the catch-all. Pinned under the measured figure on purpose; the exact ratchet gets regenerated with the repo's own script after a full run. Along the way, roadtrip_vias and roadtrip_day_tracks join the reset list every other domain's tables are on. Without that a via written by one case was still there for the next one, which is how the first version of the MCP suite passed an assertion it should have failed.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { buildAlternativeOverlays } from './alternativeOverlays'
|
||||
import { ALT_PRIMARY, ALT_SECONDARY, ALT_LABEL_PRIMARY_BG, ALT_LABEL_SECONDARY_BG } from './alternativeColors'
|
||||
import type { RouteAlternative } from '../Map/RouteCalculator'
|
||||
|
||||
/**
|
||||
* FE-ALTOVL-001..010 — turning the router's answers into something drawable.
|
||||
*
|
||||
* Two decisions live here and neither is cosmetic: which of the offered roads is
|
||||
* drawn as the one you are on, and where each label hangs. A label anchored on a
|
||||
* stretch two routes share appears pinned to the wrong road, which is the whole
|
||||
* reason this is not simply the midpoint.
|
||||
*/
|
||||
|
||||
const LABELS = { fastest: 'Fastest', current: 'Current', noMotorway: 'No motorway', noToll: 'No tolls' }
|
||||
|
||||
/** A straight line of `n` points from (0,0) heading east, offset north by `lat`. */
|
||||
function line(n: number, lat: number): [number, number][] {
|
||||
return Array.from({ length: n }, (_, i) => [lat, i / 10] as [number, number])
|
||||
}
|
||||
|
||||
function alt(over: Partial<RouteAlternative & { current?: boolean }> = {}): RouteAlternative & { current?: boolean } {
|
||||
return {
|
||||
coordinates: line(20, 0),
|
||||
duration: 3600,
|
||||
distance: 100_000,
|
||||
...over,
|
||||
} as RouteAlternative & { current?: boolean }
|
||||
}
|
||||
|
||||
describe('buildAlternativeOverlays', () => {
|
||||
it('FE-ALTOVL-001: nothing to choose between is not a choice', () => {
|
||||
// One answer drawn as an "alternative" would just double the route already
|
||||
// on the map.
|
||||
expect(buildAlternativeOverlays(undefined, LABELS)).toEqual([])
|
||||
expect(buildAlternativeOverlays([], LABELS)).toEqual([])
|
||||
expect(buildAlternativeOverlays([alt()], LABELS)).toEqual([])
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-002: the quickest is the blue one when no road is being driven yet', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ duration: 5400, coordinates: line(20, 0) }), alt({ duration: 3600, coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out[1].color).toBe(ALT_PRIMARY)
|
||||
expect(out[1].labelBg).toBe(ALT_LABEL_PRIMARY_BG)
|
||||
expect(out[0].color).toBe(ALT_SECONDARY)
|
||||
expect(out[0].labelBg).toBe(ALT_LABEL_SECONDARY_BG)
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-003: the road already being driven takes the blue, even when it is slower', () => {
|
||||
// Blue means "the road you are on". Handing it to the quickest offer instead
|
||||
// would recolour the drive the traveller deliberately bent.
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ duration: 7200, current: true, coordinates: line(20, 0) }), alt({ duration: 3600, coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out[0].color).toBe(ALT_PRIMARY)
|
||||
expect(out[0].note).toBe('Current')
|
||||
expect(out[1].color).toBe(ALT_SECONDARY)
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-004: what a road IS beats what it is not', () => {
|
||||
// A way offered because the motorway was left out says so, rather than being
|
||||
// labelled by how much slower it is — being off the motorway is the reason
|
||||
// somebody would take it.
|
||||
const out = buildAlternativeOverlays(
|
||||
[
|
||||
alt({ duration: 3600, coordinates: line(20, 0) }),
|
||||
alt({ duration: 5400, avoids: 'motorway', coordinates: line(20, 1) }),
|
||||
alt({ duration: 5400, avoids: 'toll', coordinates: line(20, 2) }),
|
||||
],
|
||||
LABELS,
|
||||
)
|
||||
expect(out.map(o => o.note)).toEqual(['Fastest', 'No motorway', 'No tolls'])
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-005: a plain slower offer carries no note at all', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ duration: 3600, coordinates: line(20, 0) }), alt({ duration: 5400, coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out[1].note).toBe('')
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-006: how much slower is measured against the quickest, and never negative', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ duration: 5400, coordinates: line(20, 0) }), alt({ duration: 3600, coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out[0].slowerThanQuickest).toBe(1800)
|
||||
expect(out[1].slowerThanQuickest).toBe(0)
|
||||
expect(out.every(o => o.slowerThanQuickest >= 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-007: a label hangs where its own road is unlike every other', () => {
|
||||
// Two roads that share both ends and part in the middle. The midpoint of the
|
||||
// shared stretch would put both labels on the same line; the point furthest
|
||||
// from the other route puts each on the stretch only it uses.
|
||||
const shared = (lat: number): [number, number][] => [
|
||||
[0, 0], [0, 1], [lat, 2], [lat, 3], [0, 4], [0, 5],
|
||||
]
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ coordinates: shared(0.5) }), alt({ coordinates: shared(-0.5) })],
|
||||
LABELS,
|
||||
)
|
||||
// Each label sits on its own detour, not on the common ends.
|
||||
expect(Math.abs(out[0].at.lat)).toBeCloseTo(0.5, 5)
|
||||
expect(Math.abs(out[1].at.lat)).toBeCloseTo(0.5, 5)
|
||||
expect(out[0].at.lat).not.toBe(out[1].at.lat)
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-008: a route with no coordinates gets a label anchor rather than crashing', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ coordinates: [] }), alt({ coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out).toHaveLength(2)
|
||||
expect(out[0].at).toEqual({ lat: 0, lng: 0 })
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-009: the drive time is carried through as text and as seconds', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[alt({ duration: 3600, distance: 42_000 }), alt({ duration: 5400, coordinates: line(20, 1) })],
|
||||
LABELS,
|
||||
)
|
||||
expect(out[0].duration).toBe(3600)
|
||||
expect(out[0].distance).toBe(42_000)
|
||||
// The label is the readable form of the same number.
|
||||
expect(out[0].label).toBeTruthy()
|
||||
expect(out[0].index).toBe(0)
|
||||
expect(out[1].index).toBe(1)
|
||||
})
|
||||
|
||||
it('FE-ALTOVL-010: only one road is ever the blue one', () => {
|
||||
const out = buildAlternativeOverlays(
|
||||
[
|
||||
alt({ duration: 3600, coordinates: line(20, 0) }),
|
||||
alt({ duration: 3600, coordinates: line(20, 1) }),
|
||||
alt({ duration: 3600, coordinates: line(20, 2) }),
|
||||
],
|
||||
LABELS,
|
||||
)
|
||||
expect(out.filter(o => o.color === ALT_PRIMARY)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { serviceMarkerHtml, serviceMarkerOuter, SERVICE_MARKER_SIZE, SERVICE_MARKER_SIZE_SELECTED } from './serviceMarker'
|
||||
import { serviceColor } from './roadtripModel'
|
||||
|
||||
/**
|
||||
* FE-SVCMARK-001..007 — the disc a service stop gets on the map.
|
||||
*
|
||||
* Both renderers need the same string: one hands it to a Leaflet divIcon, the
|
||||
* other to an element it attaches itself. What is worth pinning is that an
|
||||
* ordinary place gets nothing at all — a petrol station is something the drive
|
||||
* passes through, and giving it a place-sized marker made a day look like it had
|
||||
* twice the stops it has — and that the anchor arithmetic includes the border,
|
||||
* or the pin sits a couple of pixels off the coordinate it marks.
|
||||
*/
|
||||
|
||||
describe('serviceMarkerHtml', () => {
|
||||
it('FE-SVCMARK-001: an ordinary place gets no service marker', () => {
|
||||
// Null is the signal to fall back to the place marker, so this is what keeps
|
||||
// a destination from being drawn as a fuel stop.
|
||||
expect(serviceMarkerHtml(null, false)).toBeNull()
|
||||
expect(serviceMarkerHtml(undefined, false)).toBeNull()
|
||||
expect(serviceMarkerHtml('', false)).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-SVCMARK-002: each kind of stop gets a disc in its own colour', () => {
|
||||
// All six, not only the four the corridor picker offers: a meal and a
|
||||
// viewpoint are pauses on the way too, and the map draws them the same way.
|
||||
for (const kind of ['fuel', 'charging', 'rest_area', 'campsite', 'restaurant', 'sights']) {
|
||||
const html = serviceMarkerHtml(kind, false)
|
||||
expect(html, kind).toBeTruthy()
|
||||
expect(html, kind).toContain(serviceColor(kind))
|
||||
expect(html, kind).toContain('border-radius:50%')
|
||||
}
|
||||
})
|
||||
|
||||
it('FE-SVCMARK-003: an unknown service kind is still not drawn as one', () => {
|
||||
// isServiceStopType is the gate; a value the picker never offers must not
|
||||
// reach the renderer and produce a disc in an undefined colour.
|
||||
expect(serviceMarkerHtml('banana', false)).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-SVCMARK-004: selecting one grows it, the way a place marker confirms a click', () => {
|
||||
const plain = serviceMarkerHtml('fuel', false)!
|
||||
const selected = serviceMarkerHtml('fuel', true)!
|
||||
|
||||
expect(plain).toContain(`width:${SERVICE_MARKER_SIZE}px`)
|
||||
expect(selected).toContain(`width:${SERVICE_MARKER_SIZE_SELECTED}px`)
|
||||
expect(SERVICE_MARKER_SIZE_SELECTED).toBeGreaterThan(SERVICE_MARKER_SIZE)
|
||||
})
|
||||
|
||||
it('FE-SVCMARK-005: selected carries the heavier ring, unselected the plain shadow', () => {
|
||||
expect(serviceMarkerHtml('fuel', true)!).toContain('0 0 0 3px')
|
||||
expect(serviceMarkerHtml('fuel', false)!).not.toContain('0 0 0 3px')
|
||||
})
|
||||
|
||||
it('FE-SVCMARK-006: the icon is drawn white and inline, never a logo or a photo', () => {
|
||||
// A fuel stop used to come back from the place search carrying the
|
||||
// operator's logo, so the map showed a brand roundel where every other
|
||||
// road-trip surface showed a pump.
|
||||
const html = serviceMarkerHtml('fuel', false)!
|
||||
expect(html).toContain('<svg')
|
||||
expect(html).toContain('#fff')
|
||||
expect(html).not.toContain('<img')
|
||||
expect(html).not.toContain('background-image')
|
||||
})
|
||||
})
|
||||
|
||||
describe('serviceMarkerOuter', () => {
|
||||
it('FE-SVCMARK-007: the box an anchor centres on includes the border', () => {
|
||||
// content-box plus a 2px border on each side. Measuring the inner size would
|
||||
// leave every service pin two pixels off the coordinate it marks.
|
||||
expect(serviceMarkerOuter(false)).toBe(SERVICE_MARKER_SIZE + 4)
|
||||
expect(serviceMarkerOuter(true)).toBe(SERVICE_MARKER_SIZE_SELECTED + 4)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user