Files
TREK/client/src/components/shared/NoteFormatToolbar.test.tsx
T
Maurice 0dd07befa7 feat(planner): bring the plan tab and every trip dialog into the new look
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.
2026-09-27 21:00:19 +02:00

135 lines
5.0 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { render } from '../../../tests/helpers/render'
import userEvent from '@testing-library/user-event'
import { useRef, useState } from 'react'
import NoteFormatToolbar from './NoteFormatToolbar'
/**
* The toolbar edits somebody else's controlled field, so every test drives it
* through a real textarea rather than asserting on a callback in isolation —
* the selection handling is the part that breaks, and it only exists in the DOM.
*/
function Harness({ initial = '', onValue, customTooltips }: { initial?: string; onValue?: (v: string) => void; customTooltips?: boolean }) {
const ref = useRef<HTMLTextAreaElement | null>(null)
const [value, setValue] = useState(initial)
return (
<>
<NoteFormatToolbar textareaRef={ref} onChange={v => { setValue(v); onValue?.(v) }} customTooltips={customTooltips} />
<textarea ref={ref} aria-label="body" value={value} onChange={e => setValue(e.target.value)} />
</>
)
}
const field = () => screen.getByLabelText('body') as HTMLTextAreaElement
const select = (from: number, to: number) => {
const ta = field()
ta.focus()
ta.setSelectionRange(from, to)
}
describe('NoteFormatToolbar', () => {
it('FE-NOTEBAR-001: wraps the selection and leaves the selection on the text, not the markers', async () => {
const user = userEvent.setup()
render(<Harness initial="closes at 5pm" />)
select(0, 6)
await user.click(screen.getByRole('button', { name: /bold/i }))
expect(field().value).toBe('**closes** at 5pm')
// "closes", not "**closes**" — a second format nests instead of wrapping syntax.
// The selection is restored in a rAF, after the field holds the new text.
await waitFor(() => expect(field().value.slice(field().selectionStart, field().selectionEnd)).toBe('closes'))
})
it('FE-NOTEBAR-002: an empty selection inserts the markers and puts the caret between them', async () => {
const user = userEvent.setup()
render(<Harness initial="note" />)
select(4, 4)
await user.click(screen.getByRole('button', { name: /italic/i }))
expect(field().value).toBe('note__')
await waitFor(() => expect(field().selectionStart).toBe(5))
expect(field().selectionEnd).toBe(5)
})
it('FE-NOTEBAR-003: a list prefix applies to the line the caret is on, not the whole field', async () => {
const user = userEvent.setup()
render(<Harness initial={'first\nsecond'} />)
select(8, 8) // inside "second"
await user.click(screen.getByRole('button', { name: /bulleted list/i }))
expect(field().value).toBe('first\n- second')
})
it('FE-NOTEBAR-004: pressing the same line button again removes the prefix', async () => {
const user = userEvent.setup()
render(<Harness initial="- already a list" />)
select(5, 5)
await user.click(screen.getByRole('button', { name: /bulleted list/i }))
expect(field().value).toBe('already a list')
})
it('FE-NOTEBAR-005: the link button leaves a placeholder URL to type over', async () => {
const user = userEvent.setup()
render(<Harness initial="opening hours" />)
select(0, 13)
await user.click(screen.getByRole('button', { name: /^link$/i }))
expect(field().value).toBe('[opening hours](https://)')
})
it('FE-NOTEBAR-006: the buttons do not take focus away from the field', async () => {
const user = userEvent.setup()
render(<Harness initial="text" />)
select(0, 4)
await user.click(screen.getByRole('button', { name: /bold/i }))
// If the button had taken focus, the selection would be gone and the wrap
// would have landed at position 0 instead of around the word.
expect(field().value).toBe('**text**')
await waitFor(() => expect(document.activeElement).toBe(field()))
})
it('FE-NOTEBAR-007: does nothing when the field is not mounted', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
function Detached() {
const ref = useRef<HTMLTextAreaElement | null>(null)
return <NoteFormatToolbar textareaRef={ref} onChange={onChange} />
}
render(<Detached />)
await user.click(screen.getByRole('button', { name: /bold/i }))
expect(onChange).not.toHaveBeenCalled()
})
it('FE-NOTEBAR-008: keeps the native tooltip by default', () => {
render(<Harness />)
expect(screen.getByRole('button', { name: /bold/i })).toHaveAttribute('title', 'Bold')
})
it('FE-NOTEBAR-009: with customTooltips the name shows in the shared tooltip, and the button still keeps the focus off itself', async () => {
const user = userEvent.setup()
render(<Harness initial="text" customTooltips />)
const bold = screen.getByRole('button', { name: /bold/i })
expect(bold).not.toHaveAttribute('title')
await user.hover(bold)
expect(await screen.findByRole('tooltip')).toHaveTextContent('Bold')
select(0, 4)
await user.click(bold)
expect(field().value).toBe('**text**')
await waitFor(() => expect(document.activeElement).toBe(field()))
})
})