mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
0dd07befa7
The Plan tab was the last trip tab still in the old design. It now reads like Bookings, Costs, Lists, Files and Collab, without losing a function: - Day plan: each day is a card with a head band (day tile with weather, stay and rental pills, a "+" menu for place, stay, transport, transit and note), rows with "..." menus, white badges and a white route bar. - Places column: add, search and filters in three rows, a show dropdown, category and rating filters, and a selection bar at the foot. - Place inspector, day details and the stay editor on the shared dialog frame; the place form in the editor look with the name in the head band. - Clicking a booking or transport anywhere on the plan opens the booking detail popup first; Edit leads to the editor. The popup is one host shared with the Bookings and Transports tabs. - Files and Collab dialogs, notes, polls, links and chat moved to the same frame; shared DialogShell and dialogParts hold the building blocks. - The map hover card shows the place photo and follows the theme. Every new and reworded string is translated into all locales, and a few older English leftovers (help center, cost split, import hints) with them. Phone screens keep their own look.
190 lines
7.4 KiB
TypeScript
190 lines
7.4 KiB
TypeScript
// FE-W4FPM-001 to FE-W4FPM-014
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import type { TripFile } from '../../types'
|
|
import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render'
|
|
import type { FileManagerState } from './useFileManager'
|
|
|
|
const openFile = vi.fn(async (_url: string, _name: string) => {})
|
|
const downloadFile = vi.fn(async (_url: string, _name: string) => {})
|
|
|
|
vi.mock('../../utils/fileDownload', () => ({
|
|
openFile: (url: string, name: string) => openFile(url, name),
|
|
downloadFile: (url: string, name: string) => downloadFile(url, name),
|
|
}))
|
|
|
|
import { PdfPreviewModal } from './FileManagerPdfPreviewModal'
|
|
import { MarkdownPreviewModal } from './FileManagerMarkdownPreviewModal'
|
|
|
|
const toast = { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }
|
|
const setPreviewFile = vi.fn()
|
|
|
|
function state(overrides: Partial<FileManagerState> = {}): FileManagerState {
|
|
return {
|
|
previewFile: { id: 1, original_name: 'ticket.pdf', url: '/uploads/files/ticket.pdf' } as unknown as TripFile,
|
|
previewFileUrl: '/uploads/files/ticket.pdf?token=abc',
|
|
setPreviewFile,
|
|
toast,
|
|
t: (key: string) => key,
|
|
...overrides,
|
|
} as unknown as FileManagerState
|
|
}
|
|
|
|
beforeEach(() => {
|
|
openFile.mockReset()
|
|
openFile.mockResolvedValue(undefined)
|
|
downloadFile.mockReset()
|
|
downloadFile.mockResolvedValue(undefined)
|
|
setPreviewFile.mockClear()
|
|
Object.values(toast).forEach(f => f.mockClear())
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe('PdfPreviewModal', () => {
|
|
it('FE-W4FPM-001: portals the viewer with the signed url and the file name', () => {
|
|
const { container, baseElement } = render(<PdfPreviewModal {...state()} />)
|
|
|
|
expect(container).toBeEmptyDOMElement()
|
|
expect(screen.getByText('ticket.pdf')).toBeInTheDocument()
|
|
const object = baseElement.querySelector('object') as HTMLObjectElement
|
|
expect(object).toHaveAttribute('data', '/uploads/files/ticket.pdf?token=abc#view=FitH')
|
|
expect(object).toHaveAttribute('type', 'application/pdf')
|
|
})
|
|
|
|
it('FE-W4FPM-002: leaves the viewer source unset while no signed url exists yet', () => {
|
|
const { baseElement } = render(<PdfPreviewModal {...state({ previewFileUrl: null })} />)
|
|
|
|
expect(baseElement.querySelector('object')).not.toHaveAttribute('data')
|
|
})
|
|
|
|
it('FE-W4FPM-003: the open-in-tab button uses the unsigned url', () => {
|
|
render(<PdfPreviewModal {...state()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /openTab/ }))
|
|
|
|
expect(openFile).toHaveBeenCalledWith('/uploads/files/ticket.pdf', 'ticket.pdf')
|
|
})
|
|
|
|
it('FE-W4FPM-004: a failed open toasts the error', async () => {
|
|
openFile.mockRejectedValue(new Error('blocked'))
|
|
render(<PdfPreviewModal {...state()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /openTab/ }))
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('files.openError'))
|
|
})
|
|
|
|
it('FE-W4FPM-005: the download button hands the file to the download helper', () => {
|
|
render(<PdfPreviewModal {...state()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'files.download' }))
|
|
|
|
expect(downloadFile).toHaveBeenCalledWith('/uploads/files/ticket.pdf', 'ticket.pdf')
|
|
})
|
|
|
|
it('FE-W4FPM-006: the close button and the backdrop clear the preview, the card does not', () => {
|
|
const { baseElement } = render(<PdfPreviewModal {...state()} />)
|
|
const backdrop = baseElement.querySelector('.trek-modal-backdrop') as HTMLElement
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
expect(setPreviewFile).toHaveBeenCalledWith(null)
|
|
|
|
setPreviewFile.mockClear()
|
|
fireEvent.click(screen.getByRole('dialog'))
|
|
expect(setPreviewFile).not.toHaveBeenCalled()
|
|
|
|
fireEvent.mouseDown(backdrop)
|
|
fireEvent.click(backdrop)
|
|
expect(setPreviewFile).toHaveBeenCalledWith(null)
|
|
})
|
|
|
|
it('FE-W4FPM-007: the no-plugin fallback also opens the file', () => {
|
|
render(<PdfPreviewModal {...state()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'files.downloadPdf' }))
|
|
|
|
expect(openFile).toHaveBeenCalledWith('/uploads/files/ticket.pdf', 'ticket.pdf')
|
|
})
|
|
|
|
it('FE-W4FPM-014: the head band carries the name, open, download and close', () => {
|
|
render(<PdfPreviewModal {...state()} />)
|
|
|
|
expect(screen.getByRole('dialog', { name: 'ticket.pdf' })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /openTab/ })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: 'files.download' })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('MarkdownPreviewModal', () => {
|
|
const mdState = (overrides: Partial<FileManagerState> = {}) => state({
|
|
previewFile: { id: 2, original_name: 'notes.md', url: '/uploads/files/notes.md' } as unknown as TripFile,
|
|
previewFileUrl: '/uploads/files/notes.md?token=abc',
|
|
...overrides,
|
|
})
|
|
|
|
it('FE-W4FPM-008: fetches the markdown with credentials and renders it', async () => {
|
|
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '# Packing\n\nBring a **towel**.' }))
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
render(<MarkdownPreviewModal {...mdState()} />)
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/uploads/files/notes.md?token=abc', { credentials: 'include' })
|
|
expect(await screen.findByRole('heading', { name: 'Packing' })).toBeInTheDocument()
|
|
expect(screen.getByText('towel')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4FPM-009: shows the error copy when the fetch fails', async () => {
|
|
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline') }))
|
|
|
|
render(<MarkdownPreviewModal {...mdState()} />)
|
|
|
|
expect(await screen.findByText('files.openError')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4FPM-010: shows the error copy on a non-ok response', async () => {
|
|
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, text: async () => '' })))
|
|
|
|
render(<MarkdownPreviewModal {...mdState()} />)
|
|
|
|
expect(await screen.findByText('files.openError')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4FPM-011: skips the fetch until a signed url exists', () => {
|
|
const fetchMock = vi.fn()
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
render(<MarkdownPreviewModal {...mdState({ previewFileUrl: null })} />)
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
expect(screen.getByText('notes.md')).toBeInTheDocument()
|
|
})
|
|
|
|
it('FE-W4FPM-012: the header buttons open, download and close', async () => {
|
|
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, text: async () => 'hi' })))
|
|
render(<MarkdownPreviewModal {...mdState()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /openTab/ }))
|
|
expect(openFile).toHaveBeenCalledWith('/uploads/files/notes.md', 'notes.md')
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'files.download' }))
|
|
expect(downloadFile).toHaveBeenCalledWith('/uploads/files/notes.md', 'notes.md')
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
expect(setPreviewFile).toHaveBeenCalledWith(null)
|
|
await waitFor(() => expect(screen.getByText('hi')).toBeInTheDocument())
|
|
})
|
|
|
|
it('FE-W4FPM-013: a failed open toasts the error', async () => {
|
|
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, text: async () => '' })))
|
|
openFile.mockRejectedValue(new Error('blocked'))
|
|
render(<MarkdownPreviewModal {...mdState()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /openTab/ }))
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('files.openError'))
|
|
})
|
|
})
|