mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
aaff8f9db4
A road trip tab on the mobile trip screen: the stage as a chain, the same map underneath it, the sheets a passenger needs, and a search along the route. The addon was desktop only because road trip MODE is a data switch rather than a view switch. It rewrites `assignments` and `places` for the whole planner hook, and every mobile sheet is permanently mounted and reads those same two lists, so turning it on there brought back documented regressions. The mode flag is therefore untouched. A second flag, `roadtripFeedActive`, feeds only the routing round and does it off the STORED assignments, so the phone gets the drive without what the mode drags along with it. The map is one instance shared by the plan tab and the road trip tab, hoisted into a single JSX expression so React keeps it in the same position and the WebGL context is never torn down on a tab switch. The corridor search is scoped to what a phone can afford: a window of the day rather than all of it, a smaller tile ceiling, a capped retry pass and a deadline. The standard search is three requests where the desk makes fifteen. The categories are deliberately not narrowed, because they all ride one query per box, so a second one costs nothing. Two gaps found while wiring it up are closed here as well. The fuel search drew its offers nowhere on the phone, so the button did nothing visible; they are in the dry band now, with the detour, the spare range and an accept button. And the map credit opened as a two line ribbon across a screen the map fills: both GL engines now start it collapsed, and Leaflet, which has no collapsed mode of its own, is given one.
179 lines
8.0 KiB
TypeScript
179 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { FolderOpen, Users } from 'lucide-react'
|
|
import MMehrSheet from '../../../../src/mobile/screens/trip/sheets/MMehrSheet'
|
|
import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
|
|
import type { TripFile, TripMember } from '../../../../src/types'
|
|
import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
|
|
import { resetAllStores } from '../../../helpers/store'
|
|
import { fireEvent, render, screen, within } from '../../../helpers/render'
|
|
|
|
// FE-MOB-MEHR-001 to FE-MOB-MEHR-014
|
|
//
|
|
// The sheet takes its copy from the real TranslationProvider, so the visible
|
|
// strings are asserted in English.
|
|
|
|
const TABS = [
|
|
{ id: 'plan', label: 'Plan', icon: FolderOpen },
|
|
{ id: 'transports', label: 'Transport', icon: FolderOpen },
|
|
{ id: 'buchungen', label: 'Bookings', icon: FolderOpen },
|
|
{ id: 'finanzplan', label: 'Budget', icon: FolderOpen },
|
|
{ id: 'listen', label: 'Lists', icon: FolderOpen },
|
|
{ id: 'dateien', label: 'Files', icon: FolderOpen },
|
|
{ id: 'collab', label: 'Collaboration', icon: Users },
|
|
]
|
|
|
|
const FILES = [
|
|
{ id: 1, trip_id: 1, filename: 'a.pdf', deleted_at: null },
|
|
{ id: 2, trip_id: 1, filename: 'b.pdf', deleted_at: null },
|
|
{ id: 3, trip_id: 1, filename: 'gone.pdf', deleted_at: '2026-05-01T10:00:00Z' },
|
|
] as unknown as TripFile[]
|
|
|
|
const MEMBERS = [
|
|
{ user_id: 1, username: 'maurice', role: 'owner' },
|
|
{ user_id: 2, username: 'julien', role: 'editor' },
|
|
{ user_id: 3, username: 'guest', role: 'viewer' },
|
|
] as unknown as TripMember[]
|
|
|
|
function renderSheet(plannerOverrides: Partial<TripPlanner> = {}, shellOverrides: Partial<MTripShellApi> = {}) {
|
|
const planner = buildPlanner({
|
|
TRIP_TABS: TABS as TripPlanner['TRIP_TABS'],
|
|
files: FILES,
|
|
tripMembers: MEMBERS,
|
|
...plannerOverrides,
|
|
})
|
|
const shell = buildShell({ sheet: { id: 'mehr' }, ...shellOverrides })
|
|
render(<MMehrSheet planner={planner} shell={shell} />)
|
|
return { planner, shell }
|
|
}
|
|
|
|
describe('MMehrSheet', () => {
|
|
beforeEach(() => {
|
|
resetAllStores()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-001: stays closed while another sheet id is active', () => {
|
|
renderSheet({}, { sheet: { id: 'days' } })
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-002: opens as the "More" panel', () => {
|
|
renderSheet()
|
|
expect(screen.getByRole('dialog', { name: 'More' })).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-003: only tiles the sections that are not in the dock', () => {
|
|
renderSheet()
|
|
expect(screen.getByRole('button', { name: /Files/ })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /Collaboration/ })).toBeInTheDocument()
|
|
for (const dockLabel of ['Plan', 'Transport', 'Bookings', 'Budget', 'Lists']) {
|
|
expect(screen.queryByRole('button', { name: new RegExp(`^${dockLabel}`) })).not.toBeInTheDocument()
|
|
}
|
|
})
|
|
|
|
it('FE-MOB-MEHR-004: counts only the files that are not in the trash', () => {
|
|
renderSheet()
|
|
expect(screen.getByText('2 files')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-005: counts the trip members on the collab tile', () => {
|
|
renderSheet()
|
|
expect(screen.getByText('3 people')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-006: a plugin tile gets the neutral tint and no stat line', () => {
|
|
renderSheet({
|
|
TRIP_TABS: [...TABS, { id: 'plugin:todos', label: 'Trip To-Dos', icon: FolderOpen }] as TripPlanner['TRIP_TABS'],
|
|
})
|
|
const tile = screen.getByRole('button', { name: /Trip To-Dos/ })
|
|
expect(tile.textContent).toBe('Trip To-Dos')
|
|
expect(tile.querySelector('[style]')).toHaveStyle({ color: '#68686F' })
|
|
})
|
|
|
|
it('FE-MOB-MEHR-007: survives a tab entry without an icon component', () => {
|
|
renderSheet({ TRIP_TABS: [{ id: 'plugin:bare', label: 'Bare Plugin', icon: undefined }] as unknown as TripPlanner['TRIP_TABS'] })
|
|
const tile = screen.getByRole('button', { name: 'Bare Plugin' })
|
|
expect(tile.querySelector('svg')).toBeNull()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-008: opening a section closes the sheet and switches the trip tab', () => {
|
|
const { shell } = renderSheet()
|
|
fireEvent.click(screen.getByRole('button', { name: /Files/ }))
|
|
expect(shell.closeSheet).toHaveBeenCalledTimes(1)
|
|
expect(shell.setTrTab).toHaveBeenCalledWith('dateien')
|
|
})
|
|
|
|
it('FE-MOB-MEHR-009: the action rows open the share, export and edit sheets', () => {
|
|
const { shell } = renderSheet()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Share Trip' }))
|
|
fireEvent.click(screen.getByRole('button', { name: 'Export' }))
|
|
fireEvent.click(screen.getByRole('button', { name: 'Edit Trip' }))
|
|
expect(shell.openSheet).toHaveBeenNthCalledWith(1, 'members')
|
|
expect(shell.openSheet).toHaveBeenNthCalledWith(2, 'export')
|
|
expect(shell.openSheet).toHaveBeenNthCalledWith(3, 'tripedit')
|
|
})
|
|
|
|
it('FE-MOB-MEHR-010: drops the edit row without the trip_edit permission', () => {
|
|
const { planner } = renderSheet({ can: vi.fn(() => false) })
|
|
expect(planner.can).toHaveBeenCalledWith('trip_edit', planner.trip)
|
|
expect(screen.queryByRole('button', { name: 'Edit Trip' })).not.toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: 'Share Trip' })).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-MOB-MEHR-011: renders only the action rows when every section sits in the dock', () => {
|
|
renderSheet({
|
|
TRIP_TABS: TABS.filter(tab => tab.id !== 'dateien' && tab.id !== 'collab') as TripPlanner['TRIP_TABS'],
|
|
})
|
|
expect(screen.queryByText(/files$/)).not.toBeInTheDocument()
|
|
const rows = screen.getByRole('button', { name: 'Share Trip' }).parentElement
|
|
expect(rows).not.toHaveClass('mt-2')
|
|
})
|
|
|
|
it('FE-MOB-MEHR-012: separates the action rows from the tile grid when both are present', () => {
|
|
renderSheet()
|
|
const rows = screen.getByRole('button', { name: 'Share Trip' }).parentElement
|
|
expect(rows).toHaveClass('mt-2')
|
|
})
|
|
|
|
// Both sides read the same priority list (dockTabs.ts). They used to be two
|
|
// hand-kept copies, and extending only one showed the same section in the dock AND
|
|
// as a tile here.
|
|
describe('the dock overflow', () => {
|
|
/** The sections with the road trip addon on, in the order the planner builds them. */
|
|
const WITH_ROADTRIP = [
|
|
{ id: 'plan', label: 'Plan', icon: FolderOpen },
|
|
{ id: 'transports', label: 'Transport', icon: FolderOpen },
|
|
{ id: 'buchungen', label: 'Bookings', icon: FolderOpen },
|
|
{ id: 'roadtrip', label: 'Road trip', icon: FolderOpen },
|
|
{ id: 'listen', label: 'Lists', icon: FolderOpen },
|
|
{ id: 'finanzplan', label: 'Budget', icon: FolderOpen },
|
|
{ id: 'dateien', label: 'Files', icon: FolderOpen },
|
|
{ id: 'collab', label: 'Collaboration', icon: Users },
|
|
]
|
|
|
|
it('FE-MOB-MEHR-013: the section the drive pushed out of the dock lands here', () => {
|
|
renderSheet({ TRIP_TABS: WITH_ROADTRIP as TripPlanner['TRIP_TABS'] })
|
|
|
|
// Six sections do not fit the dock, so the packing list gives up its seat.
|
|
expect(screen.getByRole('button', { name: 'Lists' })).toBeInTheDocument()
|
|
// The drive itself never overflows: it sits second in the priority list.
|
|
expect(screen.queryByRole('button', { name: 'Road trip' })).not.toBeInTheDocument()
|
|
// Exactly the three the dock could not seat, nothing shown twice.
|
|
const grid = screen.getByRole('button', { name: 'Lists' }).parentElement
|
|
expect(within(grid as HTMLElement).getAllByRole('button')).toHaveLength(3)
|
|
})
|
|
|
|
it('FE-MOB-MEHR-014: a dock with room keeps the packing list, addon or no addon', () => {
|
|
renderSheet({
|
|
TRIP_TABS: WITH_ROADTRIP.filter(
|
|
tab => tab.id !== 'finanzplan' && tab.id !== 'dateien' && tab.id !== 'collab',
|
|
) as TripPlanner['TRIP_TABS'],
|
|
})
|
|
|
|
// Five sections still fit beside the More button, so nothing overflows and the
|
|
// sheet is the action rows alone.
|
|
expect(screen.queryByRole('button', { name: 'Lists' })).not.toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: 'Share Trip' }).parentElement).not.toHaveClass('mt-2')
|
|
})
|
|
})
|
|
})
|