Files
TREK/client/tests/unit/mobile/trip/PlTimeFields.test.tsx
T
Maurice bbe4d62c90 @
fix(export): print the reader clock format, and split window bookings in the feed

Three v4.0.0 reports that all come down to a surface printing what is stored
instead of what the reader asked for.

The PDF (#2066) is assembled as an HTML string outside React, so no hook could
reach it and no time format was ever passed in. It printed the column: four
clock sites, all 24h whatever the personal setting or the admin default said,
and a place time typed by a 12h user came out as "3:00 PM" for a 24h reader.
Both export entry points now hand the format over the way they already hand
over the locale, and the four sites go through formatClockTime.

The journey editors (#2067) used a native <input type="time">, which paints
12h or 24h from the browser locale and cannot be told otherwise by any
attribute. Every other time field in TREK has used CustomTimePicker for exactly
that reason; the journey editor, its phone sheet and the phone place sheet were
never migrated, so the same place form obeyed the setting on desktop and
ignored it on a phone. The picker gained a disabled prop so the read-only
journey sheet keeps the affordance the native input gave it.

In the subscribed calendar (#2068), a booking that is a window rather than an
activity read as one block across its whole range: a week of airport parking
sat on all seven days. Both hand-overs now stand on their own — an hour each,
carrying what the block used to say — and the block goes. Only when the two
ends fall on different days: a same-day booking is one sitting, and a rental
with only its pickup geocoded keeps the block, which is the only thing holding
the return time and its zone at all. Rental cars were missing outright, because
the planner writes reservation_time = NULL when its optional time pickers are
left blank while still setting day_id, and nothing in the exporter ever read
the linked days. It reads them now, as the last resort, for every type — so a
hand-typed bus or train shows up as well.
@
2026-08-27 18:19:33 +02:00

155 lines
6.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import PlTimeFields from '../../../../src/mobile/screens/trip/sheets/PlTimeFields'
import type { Assignment } from '../../../../src/types'
import { buildPlanner } from '../../../helpers/mobileTrip'
import { fireEvent, render, screen } from '../../../helpers/render'
import { seedStore } from '../../../helpers/store'
import { buildSettings } from '../../../helpers/factories'
import { useSettingsStore } from '../../../../src/store/settingsStore'
// FE-MOB-PLTIME-001 to FE-MOB-PLTIME-013
const planner = buildPlanner()
function assignment(
id: number,
name: string,
place_time: string | null,
end_time: string | null = null,
day_id = 1,
): Assignment {
return { id, day_id, place_id: id, order_index: 0, place: { id, name, place_time, end_time } } as unknown as Assignment
}
const SELF = assignment(1, 'Museum', '10:00', '11:00')
interface Options {
startTime?: string
endTime?: string
dayAssignments?: Assignment[]
hasTimeError?: boolean
assignmentId?: number
}
function setup({
startTime = '10:00',
endTime = '11:00',
dayAssignments = [SELF],
hasTimeError = false,
assignmentId = 1,
}: Options = {}) {
const onChange = vi.fn()
const view = render(
<PlTimeFields
planner={planner}
startTime={startTime}
endTime={endTime}
onChange={onChange}
assignmentId={assignmentId}
dayAssignments={dayAssignments}
hasTimeError={hasTimeError}
/>,
)
// Both fields are CustomTimePicker text inputs now, so they answer to its
// 24h placeholder rather than to a native time input (#2067).
// Both fields are CustomTimePicker text inputs now, and they are the only text
// inputs this component renders — the placeholder itself changes with the
// user's format, so it cannot be the hook (#2067).
const inputs = Array.from(view.container.querySelectorAll('input[type="text"]')) as HTMLInputElement[]
return { ...view, onChange, start: inputs[0], end: inputs[1] }
}
describe('PlTimeFields', () => {
it('FE-MOB-PLTIME-001: renders both labelled time inputs with their current values', () => {
const { start, end } = setup()
expect(screen.getByText('places.startTime')).toBeInTheDocument()
expect(screen.getByText('places.endTime')).toBeInTheDocument()
expect(start).toHaveValue('10:00')
expect(end).toHaveValue('11:00')
})
it('FE-MOB-PLTIME-002: editing the start reports the place_time field', () => {
const { start, onChange } = setup()
fireEvent.change(start, { target: { value: '09:15' } })
expect(onChange).toHaveBeenCalledWith('place_time', '09:15')
})
it('FE-MOB-PLTIME-003: editing the end reports the end_time field', () => {
const { end, onChange } = setup()
fireEvent.change(end, { target: { value: '12:45' } })
expect(onChange).toHaveBeenCalledWith('end_time', '12:45')
})
// #2067 — the desktop twin (PlaceFormModal) has used CustomTimePicker for ages;
// the phone kept a native time input, so the same form obeyed the setting on one
// shell and ignored it on the other.
// The normalisation back to 24h on blur belongs to CustomTimePicker and is
// covered by its own suite; what this file owns is that the phone shows the
// user's format at all, which the native input it replaced never did.
it('FE-MOB-PLTIME-013: a 12h user sees meridiem clocks, a 24h user does not', () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) })
const twelve = setup()
expect(twelve.start).toHaveValue('10:00 AM')
expect(twelve.end).toHaveValue('11:00 AM')
twelve.unmount()
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '24h' }) })
const twentyFour = setup()
expect(twentyFour.start).toHaveValue('10:00')
expect(twentyFour.end).toHaveValue('11:00')
})
it('FE-MOB-PLTIME-004: shows the end-before-start warning only when the sheet flags it', () => {
setup()
expect(screen.queryByText('places.endTimeBeforeStart')).not.toBeInTheDocument()
setup({ hasTimeError: true })
expect(screen.getByText('places.endTimeBeforeStart')).toBeInTheDocument()
})
it('FE-MOB-PLTIME-005: stays quiet without a usable start time', () => {
setup({ startTime: '', dayAssignments: [assignment(1, 'Museum', null), assignment(2, 'Park', '10:30')] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
it('FE-MOB-PLTIME-006: stays quiet when the edited assignment is not in the day', () => {
setup({ assignmentId: 99, dayAssignments: [SELF, assignment(2, 'Park', '10:30')] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
it('FE-MOB-PLTIME-007: names the overlapping places of the same day', () => {
setup({
dayAssignments: [SELF, assignment(2, 'Park', '10:30', '12:00'), assignment(3, 'Cafe', '10:45')],
})
expect(screen.getByText(/places.timeCollision/)).toHaveTextContent('places.timeCollision Park, Cafe')
})
it('FE-MOB-PLTIME-008: ignores places of other days', () => {
setup({ dayAssignments: [SELF, assignment(2, 'Park', '10:30', '12:00', 2)] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
it('FE-MOB-PLTIME-009: ignores untimed places', () => {
setup({ dayAssignments: [SELF, assignment(2, 'Park', null)] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
it('FE-MOB-PLTIME-010: back-to-back slots are not a collision', () => {
setup({ dayAssignments: [SELF, assignment(2, 'Park', '11:00', '12:00')] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
it('FE-MOB-PLTIME-011: an open-ended slot still collides with a place starting later', () => {
setup({
endTime: '',
dayAssignments: [assignment(1, 'Museum', '10:00'), assignment(2, 'Park', '09:00', '10:30')],
})
expect(screen.getByText(/places.timeCollision/)).toHaveTextContent('Park')
})
it('FE-MOB-PLTIME-012: a half-typed end time is discarded instead of widening the slot', () => {
// With a complete 11:00 end the 10:30 place would overlap — '11:0' must not count.
setup({ endTime: '11:0', dayAssignments: [SELF, assignment(2, 'Park', '10:30', '12:00')] })
expect(screen.queryByText(/places.timeCollision/)).not.toBeInTheDocument()
})
})