Files
TREK/client/tests/unit/mobile/trip/MTripSheets.members.test.tsx
Maurice 49afee161d feat(mobile): phone styling for tours list, trip settings and admin cards
The tours list in the places browser now uses the same rows as places: a
grade or type tile, the day plan's figure badges and a day badge or plus on
the right. A shared "Planned on desktop" badge replaces the grey hint box
there and sits under the road trip's empty stage.

Trip settings and trip edit open phone sheets instead of the desktop
dialogs; the edit sheet gains the day count of a dateless trip and the
reminder. Questions are asked in phone sheets, not the browser.

Admin on the phone: the paddle regions and school holiday cards are phone
cards on their shared hooks, long addon descriptions fold to two lines,
the addon header keeps the wordmark inside its sentence, the group bands
drop the dash and the permissions actions get a row of their own. The
Tours addon gets its catalog name and description in every locale.

The journey map on the phone shows every layer without the switch bar.
2026-10-22 21:26:41 +02:00

103 lines
5.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { server } from '../../../helpers/msw/server'
import { useAuthStore } from '../../../../src/store/authStore'
import { useTripStore } from '../../../../src/store/tripStore'
import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
import { buildTrip, buildUser } from '../../../helpers/factories'
import { resetAllStores, seedStore } from '../../../helpers/store'
import { render, screen, waitFor } from '../../../helpers/render'
// FE-MOB-SHOST-030 to FE-MOB-SHOST-031 (001 to 029 and 032 live in MTripSheets.test.tsx)
//
// The Mehr sheet opens the phone's own MTripSettingsSheet through this host. Every
// other sheet is stubbed away so the real sheet is what the phone user gets (#2478).
vi.mock('../../../../src/mobile/screens/trip/sheets/MPlaceSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MDaySheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MDaysSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MAccommodationSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MTransportSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MBrowseActionsSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MMehrSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MExportSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MNoteSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MImportSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MPlaceEditSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MReservationSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MTransportFormSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/sheets/MCostSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtStopSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtStaySheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtKindSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtInfoSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtCorridorSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/trip/roadtrip/MRtDraftSheet', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/settings/MConfirmSheet', () => ({ default: () => null }))
vi.mock('../../../../src/components/Planner/BookingImportModal', () => ({ default: () => null }))
vi.mock('../../../../src/components/Planner/AirTrailImportModal', () => ({ default: () => null }))
vi.mock('../../../../src/components/Planner/TransitJourneyModal', () => ({ default: () => null }))
vi.mock('../../../../src/mobile/screens/dashboard/MNewTripSheet', () => ({ default: () => null }))
import MTripSheets from '../../../../src/mobile/screens/trip/sheets/MTripSheets'
const owner = buildUser({ id: 1, username: 'owner' })
const alice = buildUser({ id: 2, username: 'alice' })
function openShareSheet() {
const planner = buildPlanner()
render(<MTripSheets planner={planner} shell={buildShell({ sheet: { id: 'members' } })} />)
return { planner }
}
describe('MTripSheets share sheet', () => {
beforeEach(() => {
resetAllStores()
seedStore(useAuthStore, { user: owner, isAuthenticated: true })
seedStore(useTripStore, { trip: buildTrip({ id: 1, title: 'Japan 2026', user_id: owner.id }) })
server.use(
http.get('/api/trips/1/members', () =>
HttpResponse.json({ owner: { id: owner.id, username: owner.username, avatar_url: null }, members: [], current_user_id: owner.id }),
),
http.get('/api/trips/1/share-link', () => HttpResponse.json({ token: null })),
http.get('/api/trips/1/invite-link', () => HttpResponse.json({ token: null })),
http.get('/api/auth/users', () => HttpResponse.json({ users: [alice] })),
)
})
it('FE-MOB-SHOST-030: the user picked on the phone shows in the field before the invite is sent', async () => {
const user = userEvent.setup()
openShareSheet()
const trigger = (await screen.findByText('Select user…')).closest('button') as HTMLButtonElement
await user.click(trigger)
await user.click(await screen.findByRole('button', { name: 'alice' }))
expect(trigger).toHaveTextContent('alice')
expect(screen.queryByText('Select user…')).toBeNull()
expect(screen.getByRole('button', { name: 'Invite' })).toBeEnabled()
})
it('FE-MOB-SHOST-031: sending the invite adds the pick and frees the field again', async () => {
const user = userEvent.setup()
let body: unknown = null
server.use(
http.post('/api/trips/1/members', async ({ request }) => {
body = await request.json()
return HttpResponse.json({ success: true })
}),
)
const { planner } = openShareSheet()
const trigger = (await screen.findByText('Select user…')).closest('button') as HTMLButtonElement
await user.click(trigger)
await user.click(await screen.findByRole('button', { name: 'alice' }))
await user.click(screen.getByRole('button', { name: 'Invite' }))
await waitFor(() => expect(planner.refreshMembers).toHaveBeenCalled())
expect(body).toEqual({ identifier: 'alice' })
expect(trigger).toHaveTextContent('Select user…')
})
})