Files
TREK/client/tests/unit/mobile/trip/MPlanTimelineRows.test.tsx
T
Maurice ce37e8e97c Rich formatting and colours for notes (#1889)
* Rich formatting and colours for notes

Asked for in discussion #1629: notes should carry formatting, links and
colour, so a warning reads differently from a recommendation.

Notes have rendered Markdown for a long time. The problem was that the
only way to write it was a bare textarea, so unless you already knew the
syntax there was nothing to tell you it existed. Both note fields get a
formatting bar - bold, italic, strikethrough, code, link, lists, quote -
which edits Markdown rather than replacing it with a rich-text model, so
the stored value stays plain text that the PDF, the plugin API and every
existing note keep reading unchanged.

Links now open in their own tab. A bare anchor navigates the planner
away, which loses the open day and the map, and without rel=noopener the
opened page can navigate this one - notes are content other trip members
wrote.

Day notes get a colour from a short shared palette. It tints the card,
the border and the icon circle, and it reaches the PDF as the spine and
rule rather than a fill. The dialog is two columns now, with a live
preview of the card beside the picker, because a row of swatches says
nothing about what a tinted note looks like next to a place.

On the phone the thirty-two icon grid was most of the screen, so it is
folded behind the chosen icon, which doubles as the colour preview. The
detail field there got the same formatting bar and the same 2000
character ceiling as the desktop one - they write the same column.

* Fix the tests that pinned the old note caps, and rework the note actions

The e2e suite builds its own day_notes table by hand rather than going
through schema.ts, so it never got the new column and every note insert
came back 500. Three more tests wrote down the 250 character body cap
and the old create() arity.

While in there: the edit and delete buttons on a note card were a 10px
icon in a 14px box, side by side, and they took width away from the text
on every note whether you were hovering or not. They are stacked in a
capsule now, 30px targets with their own hover states, floating over the
right edge of the card only while the pointer is on it. The text keeps
the full width the rest of the time.

* Centre the day badge when there is no weather to stack under it

The badge is pinned to the top of the row because with weather it is a
tall stack that has to start there. Without a location there is no
weather block, so all that is left is a 26px circle floating above the
day title.

* Render the place subtitle in the places list, and drop the form hint

The row in the places sidebar printed the description raw while the same
line in the day plan renders it, so a place written with the formatting
bar read as _underscores_ in one list and italics in the other. Still
clamped to one line - it is a list entry, not the inspector.

The "Markdown supported" line under the notes field goes: the bar above
it already says so.
2026-08-11 22:14:12 +02:00

523 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen } from '../../../helpers/render'
import {
ConnRow, HotelConnRow, NoteRow, PlaceRow, PlanScheduleRow, ReorderStack, TransitRow, TransportRow,
} from '../../../../src/mobile/screens/trip/plan/MPlanTimelineRows'
import type { TransitMeta, TransportEntry } from '../../../../src/mobile/screens/trip/plan/planTimelineModel'
import type { PluginDayScheduleItem } from '../../../../src/api/client'
import type { Assignment, DayNote, Place, Reservation, RouteSegment, TranslationFn } from '../../../../src/types'
// FE-MOB-PLROW-001 to FE-MOB-PLROW-041
// Same echo strategy as tests/helpers/mobileTrip: assertions stay on keys, not copy.
const t: TranslationFn = (key, params) =>
params ? `${key}:${Object.values(params).join(',')}` : key
const chrome = (editing = false) => ({ editing, t, language: 'en', timeFormat: '24h' })
const REORDER = <span data-testid="reorder">reorder</span>
function assignment(over: Record<string, unknown> = {}): Assignment {
return {
id: 11,
day_id: 2,
place_id: 101,
order_index: 0,
place: {
id: 101,
name: 'Museum',
place_time: '09:30',
address: 'Museum Rd 1',
lat: 35.71,
lng: 139.79,
category: { icon: 'Landmark', color: '#a855f7' },
},
...over,
} as unknown as Assignment
}
const SEG: RouteSegment = {
mid: [35.7, 139.7], from: [35.71, 139.79], to: [35.65, 139.7],
distance: 5400, duration: 900,
walkingText: '1 h 5 min', drivingText: '14 min', distanceText: '5.4 km',
}
describe('ReorderStack', () => {
it('FE-MOB-PLROW-001: renders both arrows with their labels and fires them', () => {
const onUp = vi.fn()
const onDown = vi.fn()
render(<ReorderStack onUp={onUp} onDown={onDown} canUp canDown t={t} />)
fireEvent.click(screen.getByLabelText('dayplan.moveUp'))
fireEvent.click(screen.getByLabelText('dayplan.moveDown'))
expect(onUp).toHaveBeenCalledTimes(1)
expect(onDown).toHaveBeenCalledTimes(1)
})
it('FE-MOB-PLROW-002: disables the arrow that would fall off the list', () => {
render(<ReorderStack onUp={vi.fn()} onDown={vi.fn()} canUp={false} canDown={false} t={t} />)
expect(screen.getByLabelText('dayplan.moveUp')).toBeDisabled()
expect(screen.getByLabelText('dayplan.moveDown')).toBeDisabled()
})
it('FE-MOB-PLROW-003: stops the click from opening the row it sits in', () => {
const rowClick = vi.fn()
const onUp = vi.fn()
render(
<div onClick={rowClick}>
<ReorderStack onUp={onUp} onDown={vi.fn()} canUp canDown t={t} />
</div>,
)
fireEvent.click(screen.getByLabelText('dayplan.moveUp'))
expect(onUp).toHaveBeenCalled()
expect(rowClick).not.toHaveBeenCalled()
})
})
describe('PlaceRow', () => {
const props = {
assignment: assignment(),
fullPlace: undefined,
linkedRes: null,
chrome: chrome(),
reorder: REORDER,
onOpen: vi.fn(),
onEdit: vi.fn(),
onRemove: vi.fn(),
}
it('FE-MOB-PLROW-004: shows name, formatted time and the address subtitle', () => {
render(<PlaceRow {...props} onOpen={vi.fn()} />)
expect(screen.getByText('Museum')).toBeInTheDocument()
expect(screen.getByText('09:30')).toBeInTheDocument()
expect(screen.getByText('Museum Rd 1')).toBeInTheDocument()
})
it('FE-MOB-PLROW-005: falls back to the description when there is no address', () => {
const a = assignment({
place: { id: 101, name: 'Museum', address: null, description: 'Ukiyo-e wing' },
})
render(<PlaceRow {...props} assignment={a} />)
expect(screen.getByText('Ukiyo-e wing')).toBeInTheDocument()
})
it('FE-MOB-PLROW-006: drops the meta line entirely when there is neither time nor subtitle', () => {
const a = assignment({ place: { id: 101, name: 'Museum' } })
const { container } = render(<PlaceRow {...props} assignment={a} />)
expect(container.textContent).toBe('Museum')
})
it('FE-MOB-PLROW-007: a linked booking replaces the subtitle and adds the booking badge', () => {
const linkedRes = { id: 31, status: 'confirmed', confirmation_number: 'X9K' } as unknown as Reservation
render(<PlaceRow {...props} linkedRes={linkedRes} />)
expect(screen.getByText('dayplan.confirmed · #X9K')).toBeInTheDocument()
expect(screen.getByText('mobileTrip.resBadge')).toBeInTheDocument()
})
it('FE-MOB-PLROW-008: a pending booking without a number shows just the status', () => {
const linkedRes = { id: 31, status: 'pending', confirmation_number: null } as unknown as Reservation
render(<PlaceRow {...props} linkedRes={linkedRes} />)
expect(screen.getByText('dayplan.pendingRes')).toBeInTheDocument()
})
it('FE-MOB-PLROW-009: renders the pool photo of the place in go mode', () => {
const fullPlace = {
id: 101, name: 'Museum', image_url: '/uploads/places/museum.jpg',
google_place_id: null, osm_id: null, lat: 35.71, lng: 139.79,
} as unknown as Place
render(<PlaceRow {...props} fullPlace={fullPlace} />)
expect(screen.getByAltText('Museum')).toHaveAttribute('src', '/uploads/places/museum.jpg')
})
it('FE-MOB-PLROW-010: falls back to the assignment projection when the pool entry is missing', () => {
const a = assignment({
place: { id: 101, name: 'Museum', image_url: '/uploads/places/thumb.jpg', category: null },
})
render(<PlaceRow {...props} assignment={a} fullPlace={undefined} />)
expect(screen.getByAltText('Museum')).toHaveAttribute('src', '/uploads/places/thumb.jpg')
})
it('FE-MOB-PLROW-011: tapping the row opens the place', () => {
const onOpen = vi.fn()
render(<PlaceRow {...props} onOpen={onOpen} />)
fireEvent.click(screen.getByText('Museum'))
expect(onOpen).toHaveBeenCalledTimes(1)
})
it('FE-MOB-PLROW-012: edit mode swaps the avatar for edit/remove circles plus the reorder slot', () => {
const onEdit = vi.fn()
const onRemove = vi.fn()
const onOpen = vi.fn()
render(
<PlaceRow {...props} chrome={chrome(true)} onEdit={onEdit} onRemove={onRemove} onOpen={onOpen} />,
)
expect(screen.getByTestId('reorder')).toBeInTheDocument()
fireEvent.click(screen.getByLabelText('common.edit'))
fireEvent.click(screen.getByLabelText('planner.removeFromDay'))
expect(onEdit).toHaveBeenCalledTimes(1)
expect(onRemove).toHaveBeenCalledTimes(1)
// the circles stop propagation so the row does not also open
expect(onOpen).not.toHaveBeenCalled()
})
})
describe('TransportRow', () => {
const FLIGHT = {
id: 21, type: 'flight', title: 'LH 714 to Tokyo', day_id: 2,
reservation_time: '2026-05-02T08:15', reservation_end_time: '2026-05-02T18:40',
metadata: { airline: 'Lufthansa', flight_number: 'LH714', departure_airport: 'MUC', arrival_airport: 'HND' },
} as unknown as TransportEntry
const base = { dayId: 2, chrome: chrome(), reorder: REORDER, onOpen: vi.fn() }
it('FE-MOB-PLROW-013: shows a single-day booking as a start–end range with the flight subtitle', () => {
const { container } = render(<TransportRow {...base} res={FLIGHT} />)
expect(screen.getByText('LH 714 to Tokyo')).toBeInTheDocument()
expect(container.textContent).toContain('08:15 – 18:40')
expect(screen.getByText('Lufthansa · LH714 · MUC → HND')).toBeInTheDocument()
})
it('FE-MOB-PLROW-014: a multi-day span shows only the day-relevant time', () => {
const car = {
id: 22, type: 'car', title: 'Rental car', day_id: 1, end_day_id: 3,
reservation_time: '2026-05-01T09:00', reservation_end_time: '2026-05-03T17:00',
location: 'Hertz Shinjuku',
} as unknown as TransportEntry
const { container, unmount } = render(<TransportRow {...base} dayId={1} res={car} />)
expect(container.textContent).toContain('09:00')
expect(container.textContent).not.toContain('–')
expect(screen.getByText('Hertz Shinjuku')).toBeInTheDocument()
unmount()
const dropOff = render(<TransportRow {...base} dayId={3} res={car} />)
expect(dropOff.container.textContent).toContain('17:00')
})
it('FE-MOB-PLROW-015: a per-leg row is subtitled with its own from → to', () => {
const leg = { ...FLIGHT, __leg: { index: 1, total: 2, from: 'FRA', to: 'HND' } } as TransportEntry
render(<TransportRow {...base} res={leg} />)
expect(screen.getByText('FRA → HND')).toBeInTheDocument()
})
it('FE-MOB-PLROW-016: a per-leg row without endpoints has no subtitle', () => {
const leg = { ...FLIGHT, __leg: { index: 0, total: 2, from: null, to: null } } as TransportEntry
const { container } = render(<TransportRow {...base} res={leg} />)
expect(container.textContent).not.toContain('Lufthansa')
})
it('FE-MOB-PLROW-017: a train subtitle carries number, platform and seat', () => {
const train = {
id: 23, type: 'train', title: 'ICE to Hamburg', day_id: 2,
metadata: JSON.stringify({ train_number: 'ICE 599', platform: '7', seat: '21A' }),
} as unknown as TransportEntry
render(<TransportRow {...base} res={train} />)
expect(screen.getByText('ICE 599 · Gl. 7 · Sitz 21A')).toBeInTheDocument()
})
it('FE-MOB-PLROW-018: an untimed booking of an unknown type still renders its title', () => {
const other = { id: 24, type: 'yoga_retreat', title: 'Sunrise yoga', day_id: 2 } as unknown as TransportEntry
const { container } = render(<TransportRow {...base} res={other} />)
expect(screen.getByText('Sunrise yoga')).toBeInTheDocument()
expect(container.textContent).toBe('Sunrise yoga')
})
it('FE-MOB-PLROW-019: tapping the row opens it', () => {
const onOpen = vi.fn()
render(<TransportRow {...base} res={FLIGHT} onOpen={onOpen} />)
fireEvent.click(screen.getByText('LH 714 to Tokyo'))
expect(onOpen).toHaveBeenCalledTimes(1)
})
it('FE-MOB-PLROW-020: edit mode shows the reorder slot instead of the avatar', () => {
render(<TransportRow {...base} res={FLIGHT} chrome={chrome(true)} />)
expect(screen.getByTestId('reorder')).toBeInTheDocument()
})
})
describe('TransitRow', () => {
const TRANSIT_RES = {
id: 25, type: 'transit', title: 'Shibuya → Asakusa', day_id: 2,
reservation_time: '2026-05-02T10:05', reservation_end_time: '2026-05-02T10:47',
} as unknown as TransportEntry
const TRANSIT: TransitMeta = {
transfers: 1,
duration: 2520,
legs: [
{ mode: 'WALK', duration: 240, to: { name: 'Shibuya Sta.' } },
{
mode: 'SUBWAY', line: 'G', line_color: '#FF9500', line_text_color: '#ffffff',
duration: 1500, stops: 9,
from: { name: 'Shibuya Sta.', time: '10:05', track: '3' },
to: { name: 'Asakusa Sta.', time: '10:30' },
},
{ mode: 'WALK', duration: 30, to: { name: 'Hotel' } },
],
}
const base = {
res: TRANSIT_RES, transit: TRANSIT, dayId: 2, chrome: chrome(), reorder: REORDER,
onToggle: vi.fn(), onOpenJourney: vi.fn(),
}
it('FE-MOB-PLROW-021: collapsed row shows the from → to headline and the time range', () => {
const { container } = render(<TransitRow {...base} open={false} />)
expect(screen.getByText('Shibuya → Asakusa')).toBeInTheDocument()
expect(container.textContent).toContain('10:05 – 10:47')
})
it('FE-MOB-PLROW-022: the strip drops sub-minute walks and keeps the line badge', () => {
render(<TransitRow {...base} open={false} />)
// one badge only — the collapsed strip; the 30s walk is filtered out
expect(screen.getAllByText('G')).toHaveLength(1)
expect(screen.queryByText('transit.min:1 · transit.walkTo:Hotel')).not.toBeInTheDocument()
})
it('FE-MOB-PLROW-041: a walk without a duration stays in the strip, like in the expanded list', () => {
const transit: TransitMeta = {
legs: [
{ mode: 'WALK', to: { name: 'Shibuya Sta.' } },
{ mode: 'SUBWAY', line: 'G', duration: 1500, from: { name: 'Shibuya Sta.' }, to: { name: 'Asakusa Sta.' } },
],
}
const { container } = render(<TransitRow {...base} transit={transit} open={false} />)
// the strip renders both legs: the footprint chip plus the line badge
const strip = container.querySelector('.flex-wrap') as HTMLElement
expect(strip.children).toHaveLength(2)
expect(screen.getByText('G')).toBeInTheDocument()
})
it('FE-MOB-PLROW-023: expanding renders every leg with duration, stops and platform', () => {
render(<TransitRow {...base} open />)
expect(screen.getAllByText('G')).toHaveLength(2)
expect(screen.getByText('transit.min:4 · transit.walkTo:Shibuya Sta.')).toBeInTheDocument()
expect(screen.getByText('Shibuya Sta. → Asakusa Sta.')).toBeInTheDocument()
expect(screen.getByText('transit.min:25 · transit.stops:9 · transit.platform:3')).toBeInTheDocument()
expect(screen.getByText('transit.min:1 · transit.walkTo:Hotel')).toBeInTheDocument()
})
it('FE-MOB-PLROW-024: tapping the header toggles the row', () => {
const onToggle = vi.fn()
render(<TransitRow {...base} open={false} onToggle={onToggle} />)
fireEvent.click(screen.getByText('Shibuya → Asakusa'))
expect(onToggle).toHaveBeenCalledTimes(1)
})
it('FE-MOB-PLROW-025: an untimed single-leg journey falls back to the raw title and the mode badge', () => {
const res = { id: 26, type: 'transit', title: 'Airport shuttle', day_id: 2 } as unknown as TransportEntry
const transit: TransitMeta = { legs: [{ mode: 'BUS', line: null, duration: 0, from: {}, to: {} }] }
const { container } = render(<TransitRow {...base} res={res} transit={transit} open />)
expect(screen.getByText('Airport shuttle')).toBeInTheDocument()
expect(screen.getAllByText('BUS')).toHaveLength(2)
expect(container.textContent).not.toContain('10:05')
})
it('FE-MOB-PLROW-038: survives uncoloured lines, missing durations and unnamed stops', () => {
const res = {
id: 27, type: 'transit', title: 'Depot → Bridge', day_id: 2, reservation_time: '2026-05-02T06:00',
} as unknown as TransportEntry
const transit: TransitMeta = {
legs: [
{ mode: 'TRAM', line: '17', from: { name: 'Depot' }, to: { name: 'Bridge' } },
{ mode: 'WALK', duration: 600 },
],
}
const { container } = render(<TransitRow {...base} res={res} transit={transit} open />)
expect(container.textContent).toContain('06:00')
// no end time — the chip stays a single value
expect(container.textContent).not.toContain('–')
// once as the row headline, once as the expanded leg
expect(screen.getAllByText('Depot → Bridge')).toHaveLength(2)
expect(screen.getByText('transit.min:10 · transit.walkTo:')).toBeInTheDocument()
})
it('FE-MOB-PLROW-026: edit mode opens the journey view and keeps the reorder slot', () => {
const onOpenJourney = vi.fn()
render(<TransitRow {...base} open={false} chrome={chrome(true)} onOpenJourney={onOpenJourney} />)
expect(screen.getByTestId('reorder')).toBeInTheDocument()
fireEvent.click(screen.getByLabelText('common.edit'))
expect(onOpenJourney).toHaveBeenCalledTimes(1)
})
})
describe('ConnRow', () => {
it('FE-MOB-PLROW-027: defaults to the driving duration and the distance', () => {
const { container } = render(<ConnRow seg={SEG} />)
expect(screen.getByText('14 min')).toBeInTheDocument()
expect(screen.getByText('· 5.4 km')).toBeInTheDocument()
expect(container.querySelector('button')).toBeNull()
})
it('FE-MOB-PLROW-028: a walking leg shows the walking duration', () => {
render(<ConnRow seg={{ ...SEG, mode: 'walking' }} />)
expect(screen.getByText('1 h 5 min')).toBeInTheDocument()
})
it('FE-MOB-PLROW-029: a plugin leg uses its own duration text and note', () => {
render(<ConnRow seg={{ ...SEG, mode: 'plugin:ev/fastest', durationText: '22 min', noteText: '25 min charge' }} />)
expect(screen.getByText('22 min')).toBeInTheDocument()
expect(screen.getByText('25 min charge')).toBeInTheDocument()
})
it('FE-MOB-PLROW-030: becomes a button when the leg mode can be changed', () => {
const onTap = vi.fn()
render(<ConnRow seg={SEG} onTap={onTap} />)
fireEvent.click(screen.getByRole('button'))
expect(onTap).toHaveBeenCalledTimes(1)
})
})
describe('PlanScheduleRow', () => {
const item = (over: Partial<PluginDayScheduleItem> = {}): PluginDayScheduleItem => ({
pluginId: 'ev', id: 'charge-1', dayId: 2, minutes: 95, label: 'Charging stop', tone: 'success',
...over,
})
it('FE-MOB-PLROW-031: renders the label with formatted minutes and the tone colour', () => {
const { container } = render(<PlanScheduleRow item={item()} />)
expect(screen.getByText('Charging stop')).toBeInTheDocument()
expect(container.textContent).toContain('1 h 35 min')
expect(container.querySelector('svg')).toHaveStyle({ color: '#10b981' })
})
it('FE-MOB-PLROW-032: an unknown tone falls back to the default colour and minutes stay optional', () => {
const { container } = render(
<PlanScheduleRow item={item({ minutes: undefined, tone: 'chartreuse' as unknown as PluginDayScheduleItem['tone'] })} />,
)
expect(container.textContent).toBe('Charging stop')
expect(container.querySelector('svg')).toHaveStyle({ color: '#4F46E5' })
})
})
describe('HotelConnRow', () => {
it('FE-MOB-PLROW-033: puts the hotel above the leg at the top of the day and below it at the bottom', () => {
const { container, unmount } = render(<HotelConnRow seg={SEG} name="Hotel Sacher" placement="top" />)
const top = container.firstElementChild as HTMLElement
expect(top.children[0].textContent).toBe('Hotel Sacher')
expect(top.children[1].textContent).toContain('14 min')
expect(top.children[1].textContent).toContain('· 5.4 km')
unmount()
const bottom = render(<HotelConnRow seg={SEG} name="Hotel Sacher" placement="bottom" />)
.container.firstElementChild as HTMLElement
expect(bottom.children[0].textContent).toContain('14 min')
expect(bottom.children[1].textContent).toBe('Hotel Sacher')
})
it('FE-MOB-PLROW-039: a bookend leg reads its travel mode like every other connector', () => {
const seg = { ...SEG, mode: 'plugin:ev/fastest', durationText: '22 min', noteText: '25 min charge' }
render(<HotelConnRow seg={seg} name="Hotel Sacher" placement="top" />)
expect(screen.getByText('22 min')).toBeInTheDocument()
expect(screen.getByText('25 min charge')).toBeInTheDocument()
expect(screen.queryByText('1 h 5 min')).not.toBeInTheDocument()
})
it('FE-MOB-PLROW-040: a walking bookend leg shows the walking duration', () => {
render(<HotelConnRow seg={{ ...SEG, mode: 'walking' }} name="Hotel Sacher" placement="bottom" />)
expect(screen.getByText('1 h 5 min')).toBeInTheDocument()
expect(screen.queryByText('14 min')).not.toBeInTheDocument()
})
})
describe('NoteRow', () => {
const note = (over: Record<string, unknown> = {}): DayNote => ({
id: 41, day_id: 2, text: 'Buy museum tickets\nCash only', time: '09:30 at the kiosk',
icon: 'Ticket', sort_order: 0,
...over,
} as unknown as DayNote)
const base = { chrome: chrome(), reorder: REORDER, onEdit: vi.fn() }
it('FE-MOB-PLROW-034: splits the leading time off and keeps the rest of the note below it', () => {
render(<NoteRow {...base} note={note()} />)
expect(screen.getByText('09:30')).toBeInTheDocument()
expect(screen.getByText('Buy museum tickets')).toBeInTheDocument()
// Two separate blocks since #1629: the extra title lines are plain text, the
// detail is rendered Markdown, so they cannot be joined into one string.
expect(screen.getByText('Cash only')).toBeInTheDocument()
expect(screen.getByText('at the kiosk')).toBeInTheDocument()
})
it('FE-MOB-PLROW-039: renders the detail as Markdown and tints the row with the note colour (#1629)', () => {
render(<NoteRow {...base} note={note({ text: 'Ferry', time: 'book **early**', color: '#dc2626' } as never)} />)
expect(screen.getByText('early').tagName).toBe('STRONG')
const card = screen.getByText('Ferry').closest('div[style]') as HTMLElement
expect(card.style.background).toContain('220, 38, 38')
})
it('FE-MOB-PLROW-035: a free-text time stays in the subtitle and shows no time chip', () => {
render(<NoteRow {...base} note={note({ text: 'Pack the day bag', time: 'before leaving' })} />)
expect(screen.getByText('before leaving')).toBeInTheDocument()
expect(screen.queryByText('09:30')).not.toBeInTheDocument()
})
it('FE-MOB-PLROW-036: a bare one-line note renders title only', () => {
const { container } = render(<NoteRow {...base} note={note({ text: 'Pack the day bag', time: null })} />)
expect(container.textContent).toBe('Pack the day bag')
})
it('FE-MOB-PLROW-037: is inert in go mode and editable in edit mode', () => {
const onEdit = vi.fn()
const { unmount } = render(<NoteRow {...base} note={note()} onEdit={onEdit} />)
fireEvent.click(screen.getByText('Buy museum tickets'))
expect(onEdit).not.toHaveBeenCalled()
unmount()
render(<NoteRow {...base} note={note()} chrome={chrome(true)} onEdit={onEdit} />)
fireEvent.click(screen.getByText('Buy museum tickets'))
expect(onEdit).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('reorder')).toBeInTheDocument()
})
})