mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
5b4a7e82ff
The trip planner and the journal get a "From Dawarich" list: the stays recorded for those dates, each one a row that can be dismissed or turned into a place or an entry after a review step that shows time, name, note and day. A place that came from a recording carries the Dawarich mark next to its name so its origin stays visible afterwards. The recorded route lies over the trip and journal maps as an overlay, coloured per day and cut along the reader's own timezone rather than UTC. It is fetched for the window on screen and never stored. The Atlas gets its own panel beside the map for the countries and cities the recordings prove, and the wish list is ticked off there, on the day of the visit, undoably. The phone has its own take on all of it, in the mobile shell's colours and with native sheets.
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
// FE-DAWARICH-SUGMODEL-001 to FE-DAWARICH-SUGMODEL-012
|
||
import { describe, it, expect } from 'vitest'
|
||
import type { DawarichSuggestion } from '@trek/shared'
|
||
import {
|
||
clockOf,
|
||
formatDayHeading,
|
||
formatDayOption,
|
||
formatDuration,
|
||
groupByDay,
|
||
timeRange,
|
||
} from './dawarichSuggestionModel'
|
||
|
||
function stay(over: Partial<DawarichSuggestion> & { id: number; localDate: string }): DawarichSuggestion {
|
||
return {
|
||
id: over.id,
|
||
sourceVisitId: String(over.id),
|
||
tripId: over.tripId ?? null,
|
||
tripTitle: over.tripTitle ?? null,
|
||
name: over.name ?? 'Cafe Reichard',
|
||
lat: over.lat ?? 50.94,
|
||
lng: over.lng ?? 6.96,
|
||
startedAt: over.startedAt ?? `${over.localDate}T10:15:00+02:00`,
|
||
endedAt: over.endedAt ?? `${over.localDate}T12:40:00+02:00`,
|
||
durationMinutes: over.durationMinutes ?? 145,
|
||
localDate: over.localDate,
|
||
sourceStatus: over.sourceStatus ?? 'suggested',
|
||
confidence: over.confidence ?? null,
|
||
confidenceBand: over.confidenceBand ?? null,
|
||
state: over.state ?? 'new',
|
||
target: over.target ?? null,
|
||
acceptedPlaceId: over.acceptedPlaceId ?? null,
|
||
acceptedJournalEntryId: over.acceptedJournalEntryId ?? null,
|
||
acceptedBucketListItemId: over.acceptedBucketListItemId ?? null,
|
||
sourceChanged: over.sourceChanged ?? false,
|
||
sourceMissing: over.sourceMissing ?? false,
|
||
matchedBucketListItemId: over.matchedBucketListItemId ?? null,
|
||
matchedBucketListName: over.matchedBucketListName ?? null,
|
||
countryCode: over.countryCode ?? null,
|
||
firstSeenAt: over.firstSeenAt ?? '2026-09-10T06:00:00Z',
|
||
lastSeenAt: over.lastSeenAt ?? '2026-09-10T06:00:00Z',
|
||
}
|
||
}
|
||
|
||
const t = (key: string, params?: Record<string, unknown>): string =>
|
||
`${key}:${JSON.stringify(params ?? {})}`
|
||
|
||
describe('groupByDay', () => {
|
||
it('FE-DAWARICH-SUGMODEL-001: newest day first, earliest stay first within a day', () => {
|
||
const days = groupByDay([
|
||
stay({ id: 1, localDate: '2026-09-10', startedAt: '2026-09-10T18:00:00+02:00' }),
|
||
stay({ id: 2, localDate: '2026-09-11' }),
|
||
stay({ id: 3, localDate: '2026-09-10', startedAt: '2026-09-10T09:00:00+02:00' }),
|
||
])
|
||
expect(days.map(d => d.date)).toEqual(['2026-09-11', '2026-09-10'])
|
||
expect(days[1].stays.map(s => s.id)).toEqual([3, 1])
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-002: the colour counts from the oldest day, like the map does', () => {
|
||
const days = groupByDay([
|
||
stay({ id: 1, localDate: '2026-09-10' }),
|
||
stay({ id: 2, localDate: '2026-09-11' }),
|
||
])
|
||
const oldest = days.find(d => d.date === '2026-09-10')!
|
||
const newest = days.find(d => d.date === '2026-09-11')!
|
||
expect(oldest.color).not.toBe(newest.color)
|
||
// Same input in a different order paints the same day the same colour.
|
||
const again = groupByDay([
|
||
stay({ id: 2, localDate: '2026-09-11' }),
|
||
stay({ id: 1, localDate: '2026-09-10' }),
|
||
])
|
||
expect(again.find(d => d.date === '2026-09-10')!.color).toBe(oldest.color)
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-003: nothing in, nothing out', () => {
|
||
expect(groupByDay([])).toEqual([])
|
||
})
|
||
})
|
||
|
||
describe('clockOf / timeRange', () => {
|
||
it('FE-DAWARICH-SUGMODEL-004: reads the clock out of the timestamp\'s own offset', () => {
|
||
expect(clockOf('2026-09-10T10:15:00+02:00')).toBe('10:15')
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-005: an unreadable timestamp has no clock', () => {
|
||
expect(clockOf('yesterday')).toBe('')
|
||
expect(clockOf('')).toBe('')
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-006: a range when both ends read, the arrival alone when the end does not', () => {
|
||
expect(timeRange('2026-09-10T10:15:00+02:00', '2026-09-10T12:40:00+02:00')).toBe('10:15 – 12:40')
|
||
expect(timeRange('2026-09-10T10:15:00+02:00', 'nonsense')).toBe('10:15')
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-007: no arrival, no range', () => {
|
||
expect(timeRange('nonsense', '2026-09-10T12:40:00+02:00')).toBe('')
|
||
})
|
||
})
|
||
|
||
describe('formatDuration', () => {
|
||
it('FE-DAWARICH-SUGMODEL-008: minutes under an hour, hours above it, both when there is a remainder', () => {
|
||
expect(formatDuration(45, t)).toBe('dawarich.duration.minutes:{"minutes":45}')
|
||
expect(formatDuration(120, t)).toBe('dawarich.duration.hours:{"hours":2}')
|
||
expect(formatDuration(145, t)).toBe('dawarich.duration.hoursMinutes:{"hours":2,"minutes":25}')
|
||
})
|
||
})
|
||
|
||
describe('formatDayHeading', () => {
|
||
it('FE-DAWARICH-SUGMODEL-009: reads as the local date, not as yesterday', () => {
|
||
// Built from the date parts rather than from a Date at midnight UTC, which
|
||
// is the difference between "Thu, 10 Sept" and "Wed, 9 Sept" west of UTC.
|
||
expect(formatDayHeading('2026-09-10', 'en-GB')).toContain('10')
|
||
expect(formatDayHeading('2026-09-10', 'en-GB')).toMatch(/Thu/)
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-010: an unparseable date is shown as it came', () => {
|
||
expect(formatDayHeading('not-a-date', 'en-GB')).toBe('not-a-date')
|
||
})
|
||
})
|
||
|
||
describe('formatDayOption', () => {
|
||
it('FE-DAWARICH-SUGMODEL-011: the day number is the label, the date rides along as a badge', () => {
|
||
const option = formatDayOption(3, '2026-09-10', 'en-GB', t)
|
||
expect(option.label).toBe('planner.dayN:{"n":3}')
|
||
expect(option.badge).toMatch(/10/)
|
||
})
|
||
|
||
it('FE-DAWARICH-SUGMODEL-012: a day without a date gets no badge rather than an empty one', () => {
|
||
expect(formatDayOption(1, null, 'en-GB', t)).toEqual({ label: 'planner.dayN:{"n":1}', badge: undefined })
|
||
})
|
||
})
|