mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
2f6f921aef
Every row of the reorder dialog, and of the days sheet on a phone, now has a delete button. It first asks, and the question lists what goes with the day: planned places stay in the place list, notes and day texts are deleted, bookings stay without a day, a stay that checks in or out on the day is cancelled with its booking and expense (shown in the danger tone), and on a dated trip the later days and their bookings move one date earlier or the trip ends a day earlier. The server side moved from a bare DELETE into DayRemovalService, which REST, the delete_day tool and the plugin RPC all use. It closes the numbering, keeps the dates on their positions like a reorder, re-stamps the moved bookings, shifts the road trip boundaries, cancels touching stays through the accommodations domain, refuses the last day of a trip, and fans out day:deleted, day:reordered and trip:updated so collaborators renumber and see the new day count and end date. The trip query moved into a leaf file so the days domain can read it without a module cycle. On the client the delete is online only, runs optimistically through the days slice with the same reducer collaborators apply, and the ConfirmDialog gained a slot for extra content, an Escape that only closes itself, and the danger and accent tokens.
147 lines
6.5 KiB
TypeScript
147 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import MDaysSheet from '../../../../src/mobile/screens/trip/sheets/MDaysSheet'
|
|
import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
|
|
import type { Day } from '../../../../src/types'
|
|
import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
|
|
import { resetAllStores } from '../../../helpers/store'
|
|
import { fireEvent, render, screen } from '../../../helpers/render'
|
|
|
|
// FE-MOB-DAYSS-001 to FE-MOB-DAYSS-015
|
|
//
|
|
// The sheet reads its copy from the real TranslationProvider (useTranslation),
|
|
// not from planner.t — assertions therefore go against the English strings.
|
|
|
|
// Deliberately unsorted: the sheet orders by day_number.
|
|
const DAYS = [
|
|
{ id: 3, trip_id: 1, day_number: 3, date: '2026-05-03', title: null },
|
|
{ id: 1, trip_id: 1, day_number: 1, date: '2026-05-01', title: null },
|
|
{ id: 2, trip_id: 1, day_number: 2, date: '2026-05-02', title: 'Old Town' },
|
|
] as unknown as Day[]
|
|
|
|
function renderSheet(plannerOverrides: Partial<TripPlanner> = {}, shellOverrides: Partial<MTripShellApi> = {}) {
|
|
const planner = buildPlanner({ days: DAYS, ...plannerOverrides })
|
|
const shell = buildShell({ sheet: { id: 'days' }, ...shellOverrides })
|
|
render(<MDaysSheet planner={planner} shell={shell} />)
|
|
return { planner, shell }
|
|
}
|
|
|
|
describe('MDaysSheet', () => {
|
|
beforeEach(() => {
|
|
resetAllStores()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-001: stays closed while another sheet id is active', () => {
|
|
renderSheet({}, { sheet: { id: 'mehr' } })
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-002: stays closed when no sheet is open at all', () => {
|
|
renderSheet({}, { sheet: null })
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-003: renders the reorder header with its hint', () => {
|
|
renderSheet()
|
|
expect(screen.getByRole('dialog', { name: 'Reorder days' })).toBeInTheDocument()
|
|
expect(screen.getByText("A day's places, notes and bookings move with it.")).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-003b: the hint wraps instead of being cut off mid-sentence', () => {
|
|
// The shared header truncates its subtitle to one line, which is right for a
|
|
// filename but cut this sentence off in every language (#1814).
|
|
renderSheet()
|
|
const hint = screen.getByText("A day's places, notes and bookings move with it.")
|
|
expect(hint).not.toHaveClass('truncate')
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-004: lists the days in day_number order and numbers the rows', () => {
|
|
renderSheet()
|
|
const positions = screen.getAllByText(/^[123]$/).map(el => el.textContent)
|
|
expect(positions).toEqual(['1', '2', '3'])
|
|
const labels = ['Fri, May 1', 'Old Town', 'Sun, May 3']
|
|
for (const label of labels) expect(screen.getByText(label)).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-005: falls back to "Day n" when neither title nor a usable date exists', () => {
|
|
const days = [
|
|
{ id: 7, trip_id: 1, day_number: 1, date: null, title: null },
|
|
{ id: 8, trip_id: 1, day_number: 2, date: 'not-a-date', title: '' },
|
|
] as unknown as Day[]
|
|
renderSheet({ days })
|
|
expect(screen.getByText('Day 1')).toBeInTheDocument()
|
|
expect(screen.getByText('Day 2')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-006: numbers a day without day_number by its list position', () => {
|
|
const days = [{ id: 9, trip_id: 1, day_number: null, date: null, title: null }] as unknown as Day[]
|
|
renderSheet({ days })
|
|
expect(screen.getByText('Day 1')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-007: moving a day up sends the whole reordered id list', () => {
|
|
const { planner } = renderSheet()
|
|
fireEvent.click(screen.getAllByRole('button', { name: 'Move up' })[1])
|
|
expect(planner.handleReorderDays).toHaveBeenCalledWith([2, 1, 3])
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-008: moving a day down sends the whole reordered id list', () => {
|
|
const { planner } = renderSheet()
|
|
fireEvent.click(screen.getAllByRole('button', { name: 'Move down' })[0])
|
|
expect(planner.handleReorderDays).toHaveBeenCalledWith([2, 1, 3])
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-009: disables the moves that would leave the list', () => {
|
|
renderSheet()
|
|
const up = screen.getAllByRole('button', { name: 'Move up' })
|
|
const down = screen.getAllByRole('button', { name: 'Move down' })
|
|
expect(up[0]).toBeDisabled()
|
|
expect(up[2]).toBeEnabled()
|
|
expect(down[2]).toBeDisabled()
|
|
expect(down[0]).toBeEnabled()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-010: appends a day', () => {
|
|
const { planner } = renderSheet()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Add day' }))
|
|
expect(planner.handleAddDay).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-011: hides every edit affordance for a read-only member', () => {
|
|
const { planner } = renderSheet({ can: vi.fn(() => false) })
|
|
expect(planner.can).toHaveBeenCalledWith('day_edit', planner.trip)
|
|
expect(screen.queryByRole('button', { name: 'Move up' })).not.toBeInTheDocument()
|
|
expect(screen.queryByRole('button', { name: 'Add day' })).not.toBeInTheDocument()
|
|
// The list itself stays readable.
|
|
expect(screen.getByText('Old Town')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-012: the header close button closes the sheet', () => {
|
|
const { shell } = renderSheet()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
expect(shell.closeSheet).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-013: each row asks the planner to delete its own day', () => {
|
|
const { planner } = renderSheet()
|
|
const buttons = screen.getAllByRole('button', { name: 'Delete day' })
|
|
expect(buttons).toHaveLength(3)
|
|
// Rows run in day_number order: the second one is day 2.
|
|
fireEvent.click(buttons[1])
|
|
expect(planner.handleDeleteDay).toHaveBeenCalledWith(2)
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-014: a read-only member gets no delete buttons', () => {
|
|
renderSheet({ can: vi.fn(() => false) })
|
|
expect(screen.queryByRole('button', { name: 'Delete day' })).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-DAYSS-015: while the planner blocks deleting, the buttons are off and the sheet says why', () => {
|
|
const { planner } = renderSheet({ deleteDayBlocked: 'A trip needs at least one day' })
|
|
const buttons = screen.getAllByRole('button', { name: 'Delete day' })
|
|
expect(buttons.every(b => (b as HTMLButtonElement).disabled)).toBe(true)
|
|
fireEvent.click(buttons[0])
|
|
expect(planner.handleDeleteDay).not.toHaveBeenCalled()
|
|
expect(screen.getByText('A trip needs at least one day')).toBeInTheDocument()
|
|
})
|
|
})
|