mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(costs): row buttons, settle-up rows and share toggles say what they are about to a screen reader
A screen reader lists a page's buttons together, and the costs views gave
it twenty "Edit" and "Delete" buttons with nothing to tell them apart.
- Edit and delete on an expense name the expense ("Edit: Dinner"), edit
and undo on a payment name who paid whom, on desktop and phone.
- A settle-up row shows only initials in its avatars, its names lived in a
tooltip; the row now carries them for assistive tech too, and its Settle
button says who pays whom and how much.
- The search field of every searchable select (currency, time zone,
category) was read out as "..."; it now says Search in the user's
language.
- The public link toggles on the phone share sheet and the journey share
toggles say whether they are on (aria-pressed), not only by colour.
- Japanese still said "you" in the settle-up lines.
Refs #2604
This commit is contained in:
@@ -123,4 +123,20 @@ describe('CostsPanel: summary footers and won amounts (#2604)', () => {
|
||||
await user.type(shares[0], '1000.5');
|
||||
expect(shares[0].value).toBe('1000.');
|
||||
});
|
||||
|
||||
it('FE-COSTS-2604-005: edit and delete on a row say which expense they act on', async () => {
|
||||
mount([owedByBob()]);
|
||||
await screen.findByText('Museum');
|
||||
expect(screen.getByRole('button', { name: 'Edit: Museum' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Delete: Museum' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COSTS-2604-006: a settle-up row and its button name who pays whom', async () => {
|
||||
mount([owedByBob()], {
|
||||
flows: [{ from: { user_id: 2, username: 'bob' }, to: { user_id: 1, username: 'alice' }, amount: 20 }],
|
||||
});
|
||||
await screen.findByText('Museum');
|
||||
expect(screen.getByRole('button', { name: /^Settle: bob → you, 20,00\s€$/ })).toBeInTheDocument();
|
||||
expect(screen.getAllByText('bob → you').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('CostsPanel — settlements in the ledger', () => {
|
||||
await screen.findByText('Dinner')
|
||||
await screen.findByText('Payment')
|
||||
// The payment row exposes an inline undo (no need to open a separate History modal).
|
||||
expect(screen.getByRole('button', { name: 'Undo' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /^Undo(:|$)/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('records a manual payment via the Add payment button', async () => {
|
||||
@@ -242,7 +242,7 @@ describe('CostsPanel — settlements in the ledger', () => {
|
||||
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
|
||||
|
||||
await screen.findByText('Lunch')
|
||||
await user.click(screen.getAllByRole('button', { name: 'Edit' })[0])
|
||||
await user.click(screen.getAllByRole('button', { name: /^Edit(:|$)/ })[0])
|
||||
await user.click(await screen.findByRole('button', { name: 'Save' }))
|
||||
|
||||
await waitFor(() => expect(patched).toBeTruthy())
|
||||
@@ -453,7 +453,7 @@ describe('CostsPanel — settlements in the ledger', () => {
|
||||
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
|
||||
|
||||
await screen.findByText('Hotel')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// Nobody paid this expense — reopening it must not silently reselect "You".
|
||||
expect(await screen.findByRole('radio', { name: 'No one paid yet' })).toBeChecked()
|
||||
@@ -579,7 +579,7 @@ describe('CostsPanel — settlements in the ledger', () => {
|
||||
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
|
||||
|
||||
await screen.findByText('Dinner')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// Loading used to be payers.find(...), which silently dropped the second payer.
|
||||
// Seeded with two decimals since #2175, localized for EUR.
|
||||
@@ -768,7 +768,7 @@ describe('CostsPanel — settlements in the ledger', () => {
|
||||
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
|
||||
|
||||
await screen.findByText('Payment')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// The stored USD amount comes back as-is (padded to two decimals, #2175),
|
||||
// not silently reread as euros.
|
||||
@@ -984,7 +984,7 @@ describe('CostsPanel — settle up', () => {
|
||||
await screen.findByText('45,00 €')
|
||||
// The "you're owed" card names who still owes me.
|
||||
expect(screen.getByText("You're owed")).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Settle' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Settle(:|$)/ }))
|
||||
|
||||
await waitFor(() => expect(posted).toHaveLength(1))
|
||||
expect(posted[0]).toMatchObject({ from_user_id: 2, to_user_id: 1, amount: 45, currency: 'EUR' })
|
||||
@@ -1018,7 +1018,7 @@ describe('CostsPanel — settle up', () => {
|
||||
server.use(http.post('/api/trips/1/budget/settlements', () => HttpResponse.json({ error: 'no' }, { status: 500 })))
|
||||
mount([], { flows: [flows[0]] })
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Settle' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: /^Settle(:|$)/ }))
|
||||
await waitFor(() => expect(addToast).toHaveBeenCalledWith('Unknown error', 'error', undefined))
|
||||
|
||||
addToast.mockClear()
|
||||
@@ -1037,10 +1037,10 @@ describe('CostsPanel — settle up', () => {
|
||||
}))
|
||||
mount([], { settlements: [{ id: 7, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00' }] })
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Undo' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: /^Undo(:|$)/ }))
|
||||
await waitFor(() => expect(deleted).toBe(1))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Undo' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Undo(:|$)/ }))
|
||||
await waitFor(() => expect(addToast).toHaveBeenCalledWith('Unknown error', 'error', undefined))
|
||||
delete window.__addToast
|
||||
})
|
||||
@@ -1252,7 +1252,7 @@ describe('CostsPanel — expense rows', () => {
|
||||
mount([dinner()])
|
||||
|
||||
await screen.findByText('Dinner')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Delete(:|$)/ }))
|
||||
|
||||
await waitFor(() => expect(addToast).toHaveBeenCalledWith('Unknown error', 'error', undefined))
|
||||
// The optimistic removal is rolled back by the store.
|
||||
@@ -1274,9 +1274,9 @@ describe('CostsPanel — expense rows', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Add expense' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Settle up' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Add payment' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Settle' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Undo' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^Settle(:|$)/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^Delete(:|$)/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^Undo(:|$)/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1353,7 +1353,7 @@ describe('CostsPanel — payment modal', () => {
|
||||
}))
|
||||
mount([], { settlements: [{ id: 7, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00' }] })
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
await user.click(await screen.findByRole('button', { name: /^Edit(:|$)/ }))
|
||||
const amount = await screen.findByPlaceholderText('0,00')
|
||||
await user.clear(amount)
|
||||
await user.type(amount, '12,50')
|
||||
@@ -1428,7 +1428,7 @@ describe('CostsPanel — payment modal', () => {
|
||||
}))
|
||||
mount([], { settlements: [{ id: 7, from_user_id: 2, to_user_id: 1, amount: 30, settled_at: '2025-06-10', created_at: '2025-06-16 10:00:00' }] })
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
await user.click(await screen.findByRole('button', { name: /^Edit(:|$)/ }))
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
||||
await waitFor(() => expect(put).toBeTruthy())
|
||||
@@ -1441,7 +1441,7 @@ describe('CostsPanel — payment modal', () => {
|
||||
const user = userEvent.setup()
|
||||
mount([], { settlements: [{ id: 7, from_user_id: 2, to_user_id: 1, amount: 30, settled_at: '2025-06-10', created_at: '2025-06-16 10:00:00' }] })
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
await user.click(await screen.findByRole('button', { name: /^Edit(:|$)/ }))
|
||||
const save = screen.getByRole('button', { name: 'Save' })
|
||||
expect(save).toBeEnabled()
|
||||
|
||||
@@ -1477,7 +1477,7 @@ describe('CostsPanel — expense modal', () => {
|
||||
mount([expense({ id: 150, name: 'Market run', category: 'groceries', total_price: 30, note, payers: [{ user_id: 1, amount: 30 }], members: [{ user_id: 1, username: 'alice', amount: 15 }, { user_id: 2, username: 'bob', amount: 15 }] })])
|
||||
|
||||
await screen.findByText('Market run')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
expect(await screen.findByDisplayValue('Apples')).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('Cake')).toBeInTheDocument()
|
||||
@@ -1499,7 +1499,7 @@ describe('CostsPanel — expense modal', () => {
|
||||
mount([expense({ id: 151, name: 'Market run', category: 'groceries', total_price: 30, note: 'TICKETJSON:{oops', payers: [{ user_id: 1, amount: 30 }], members: [{ user_id: 1, username: 'alice' }] })])
|
||||
|
||||
await screen.findByText('Market run')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Add item/i })).toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument()
|
||||
@@ -1520,7 +1520,7 @@ describe('CostsPanel — expense modal', () => {
|
||||
})])
|
||||
|
||||
await screen.findByText('Dinner')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// Custom shares reopen padded to two decimals (#2175), localized for EUR.
|
||||
expect((await screen.findByDisplayValue('70,00')).tagName).toBe('INPUT')
|
||||
@@ -1801,7 +1801,7 @@ describe('CostsPanel — remaining paths', () => {
|
||||
mount([dinner()])
|
||||
|
||||
await screen.findByText('Dinner')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Delete(:|$)/ }))
|
||||
|
||||
await waitFor(() => expect(deleted).toBe(true))
|
||||
expect(screen.queryByText('Dinner')).not.toBeInTheDocument()
|
||||
@@ -1872,7 +1872,7 @@ describe('CostsPanel — remaining paths', () => {
|
||||
)
|
||||
render(<CostsPanel tripId={1} tripMembers={[...tripMembers, { id: 3, username: 'cara', avatar_url: null }]} />)
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
await user.click(await screen.findByRole('button', { name: /^Edit(:|$)/ }))
|
||||
// No symbol is known, so the code itself prefixes the amount and labels the option.
|
||||
await screen.findByPlaceholderText('0.00')
|
||||
expect(screen.getAllByText('XBT').length).toBeGreaterThanOrEqual(2)
|
||||
@@ -1890,7 +1890,7 @@ describe('CostsPanel — remaining paths', () => {
|
||||
mount([expense({ id: 240, name: 'Mining rig', category: 'other', total_price: 2, currency: 'XBT', payers: [{ user_id: 1, amount: 2 }], members: [{ user_id: 1, username: 'alice' }] })])
|
||||
|
||||
await screen.findByText('Mining rig')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// XBT is unknown to the locale map → dot separator, two decimals (#2175).
|
||||
expect(await screen.findByDisplayValue('2.00')).toBeInTheDocument()
|
||||
@@ -2038,7 +2038,7 @@ describe('CostsPanel — split modes and guests', () => {
|
||||
})])
|
||||
|
||||
await screen.findByText('Hotel')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
|
||||
// Nobody paid, so switching to multi-payer seeds me as the only payer.
|
||||
await user.click(await screen.findByRole('button', { name: 'Multiple people paid' }))
|
||||
@@ -2159,11 +2159,11 @@ describe('CostsPanel — decimal padding on edit (#2175)', () => {
|
||||
])
|
||||
|
||||
await screen.findByText('Coffee')
|
||||
await user.click(screen.getAllByRole('button', { name: 'Edit' })[0])
|
||||
await user.click(screen.getAllByRole('button', { name: /^Edit(:|$)/ })[0])
|
||||
expect(await screen.findByDisplayValue('4,90')).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'Edit' })[1])
|
||||
await user.click(screen.getAllByRole('button', { name: /^Edit(:|$)/ })[1])
|
||||
expect(await screen.findByDisplayValue('5,00')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -2243,7 +2243,7 @@ describe('CostsPanel — negative amounts (#2176)', () => {
|
||||
// Not "Unfinished": the refund has its recipient recorded.
|
||||
expect(within(row).queryByText('Unfinished')).toBeNull()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
// The negative payer reopens selected instead of resetting to "Nobody",
|
||||
// and the total is seeded signed and padded.
|
||||
expect(await screen.findByRole('radio', { name: 'You' })).toBeChecked()
|
||||
@@ -2540,7 +2540,7 @@ describe('CostsPanel: a bill entered in the display currency (#2525)', () => {
|
||||
mount([hotel()])
|
||||
|
||||
await screen.findByText('Aparthotel Silver')
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
expect(await screen.findByDisplayValue('801.76')).toBeInTheDocument()
|
||||
// Same currency as the list, so the dialog said nothing, and 801.76 there beside
|
||||
// $791.57 in the list looked like two different expenses.
|
||||
@@ -2610,7 +2610,7 @@ describe('CostsPanel: a bill entered in the display currency (#2525)', () => {
|
||||
// The list reads the missing currency as the trip's own.
|
||||
expect(screen.getByText(/100,00\s€ → \$115\.51/)).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
await user.click(screen.getByRole('button', { name: /^Edit(:|$)/ }))
|
||||
expect(await screen.findByDisplayValue('100,00')).toBeInTheDocument()
|
||||
// Seeding the display currency here would label 100 euro as 100 dollars, and a
|
||||
// plain save would then store it that way.
|
||||
@@ -2676,7 +2676,7 @@ describe('CostsPanel: a bill the server cannot convert', () => {
|
||||
// Only the currency the settlement is missing, at the rate the browser holds.
|
||||
await waitFor(() => expect(frozen).toEqual([{ fallback_fx: { base: 'AUD', rates: { VND: 18241.3 } } }]))
|
||||
// The settlement is read again and Bob's half can be settled.
|
||||
expect(await screen.findByRole('button', { name: 'Settle' })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('button', { name: /^Settle(:|$)/ })).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/244\.50/).length).toBeGreaterThan(0)
|
||||
// Nothing on the page counts the bill as 8,920,000 dollars any more.
|
||||
expect(screen.queryByText(/\$8,920,000/)).toBeNull()
|
||||
@@ -2719,7 +2719,7 @@ describe('CostsPanel: a bill the server cannot convert', () => {
|
||||
)
|
||||
render(<CostsPanel tripId={1} tripMembers={tripMembers} />)
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Settle' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: /^Settle(:|$)/ }))
|
||||
await waitFor(() => expect(posted).toHaveLength(1))
|
||||
// The transfer freezes at the rate the settlement was labelled with, so it cancels its
|
||||
// flow to the cent.
|
||||
|
||||
@@ -109,6 +109,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
const idx = people.findIndex(p => p.id === id)
|
||||
return SPLIT_COLORS[(idx >= 0 ? idx : 0) % SPLIT_COLORS.length].gradient
|
||||
}, [people])
|
||||
const transferLabel = (fromId: number, toId: number) => `${personName(fromId)} → ${toId === me ? t('costs.youLower') : personName(toId)}`
|
||||
const initial = useCallback((id: number) => id === me ? t('costs.youShort') : nameOf(id).charAt(0).toUpperCase(), [me, nameOf, t])
|
||||
|
||||
const fmt = useCallback((v: number, c = base) => formatMoney(v, c, locale), [base, locale])
|
||||
@@ -447,14 +448,15 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{flows.map((f, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<Tooltip label={`${personName(f.from.user_id)} → ${f.to.user_id === me ? t('costs.youLower') : personName(f.to.user_id)}`}>
|
||||
<Tooltip label={transferLabel(f.from.user_id, f.to.user_id)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||||
<Avatar id={f.from.user_id} size={32} /><ArrowRight size={15} className="text-content-faint" /><Avatar id={f.to.user_id} size={32} />
|
||||
<span className="sr-only">{transferLabel(f.from.user_id, f.to.user_id)}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
||||
<span className="text-content" style={{ fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 700 }}>{fmt(f.amount)}</span>
|
||||
{canEdit && <button type="button" onClick={() => settleFlow(f.from.user_id, f.to.user_id, f.amount)} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{ padding: '7px 12px', borderRadius: 9, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, border: 0, cursor: 'pointer', fontFamily: 'inherit' }}>{t('costs.settle')}</button>}
|
||||
{canEdit && <button type="button" aria-label={`${t('costs.settle')}: ${transferLabel(f.from.user_id, f.to.user_id)}, ${fmt(f.amount)}`} onClick={() => settleFlow(f.from.user_id, f.to.user_id, f.amount)} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{ padding: '7px 12px', borderRadius: 9, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, border: 0, cursor: 'pointer', fontFamily: 'inherit' }}>{t('costs.settle')}</button>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -603,7 +605,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
* mobile card has always done it this way, and out here the buttons stop
|
||||
* competing with the amount for the right edge of the card.
|
||||
*/
|
||||
function RowActions({ onEdit, onDelete, deleteLabel }: { onEdit: () => void; onDelete: () => void; deleteLabel: string }) {
|
||||
function RowActions({ onEdit, onDelete, deleteLabel, name }: { onEdit: () => void; onDelete: () => void; deleteLabel: string; name: string }) {
|
||||
// No `background` here on purpose: an inline value would beat the :hover
|
||||
// rule in the stylesheet block below, which is how the first attempt at
|
||||
// this silently did nothing.
|
||||
@@ -615,10 +617,10 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
return (
|
||||
<div className="exp-actions bg-surface-secondary border border-edge-faint" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 3, flexShrink: 0, borderRadius: 999, padding: 5 }}>
|
||||
<Tooltip label={t('common.edit')}>
|
||||
<button type="button" aria-label={t('common.edit')} onClick={onEdit} className="text-content-muted transition-colors" style={btn}><Pencil size={13} /></button>
|
||||
<button type="button" aria-label={`${t('common.edit')}: ${name}`} onClick={onEdit} className="text-content-muted transition-colors" style={btn}><Pencil size={13} /></button>
|
||||
</Tooltip>
|
||||
<Tooltip label={deleteLabel}>
|
||||
<button type="button" aria-label={deleteLabel} onClick={onDelete} className="exp-action-danger transition-colors" style={{ ...btn, color: '#dc2626' }}><Trash2 size={13} /></button>
|
||||
<button type="button" aria-label={`${deleteLabel}: ${name}`} onClick={onDelete} className="exp-action-danger transition-colors" style={{ ...btn, color: '#dc2626' }}><Trash2 size={13} /></button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
@@ -741,7 +743,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{canEdit && RowActions({ onEdit: () => { setEditing(e); setModalOpen(true) }, onDelete: () => handleDelete(e.id), deleteLabel: t('common.delete') })}
|
||||
{canEdit && RowActions({ onEdit: () => { setEditing(e); setModalOpen(true) }, onDelete: () => handleDelete(e.id), deleteLabel: t('common.delete'), name: e.name })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -803,6 +805,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
function SettlementRow({ s }: { s: Settlement }) {
|
||||
// Booked in the trip currency like an expense, so it is explained the same way.
|
||||
const line = paymentLineOf(s, ctx, settled(s))
|
||||
const who = `${personName(s.from_user_id)} → ${personName(s.to_user_id)}`
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', gap: 8 }}>
|
||||
<div className="bg-surface-secondary border border-edge-faint exp-row" style={{ flex: 1, minWidth: 0, display: 'grid', gridTemplateColumns: isMobile ? '46px 1fr auto' : '46px minmax(200px, 1fr) minmax(0, 1.5fr) auto', gap: isMobile ? 16 : 18, alignItems: 'center', borderRadius: 18, padding: '16px 20px' }}>
|
||||
@@ -812,10 +815,10 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
{t('costs.payment')}
|
||||
{line && <span className="text-content-faint" style={{ fontWeight: 400, fontSize: 'calc(12px * var(--fs-scale-body, 1))', marginInlineStart: 10 }}>{fmt(line.entered.amount, line.entered.currency)} → {fmt(line.into.amount, line.into.currency)}</span>}
|
||||
</div>
|
||||
<Tooltip label={`${personName(s.from_user_id)} → ${personName(s.to_user_id)}`}>
|
||||
<Tooltip label={who}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
|
||||
<Avatar id={s.from_user_id} size={20} /><ArrowRight size={13} className="text-content-faint" /><Avatar id={s.to_user_id} size={20} />
|
||||
<span className="text-content-faint" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{personName(s.from_user_id)} → {personName(s.to_user_id)}</span>
|
||||
<span className="text-content-faint" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{who}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -829,10 +832,10 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
{canEdit && (
|
||||
<div className="exp-actions bg-surface-secondary border border-edge-faint" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 3, flexShrink: 0, borderRadius: 999, padding: 5 }}>
|
||||
<Tooltip label={t('common.edit')}>
|
||||
<button type="button" aria-label={t('common.edit')} onClick={() => setEditingSettlement(s)} className="text-content-muted transition-colors" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, borderRadius: 999, border: 0, cursor: 'pointer', padding: 0 }}><Pencil size={13} /></button>
|
||||
<button type="button" aria-label={`${t('common.edit')}: ${who}`} onClick={() => setEditingSettlement(s)} className="text-content-muted transition-colors" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, borderRadius: 999, border: 0, cursor: 'pointer', padding: 0 }}><Pencil size={13} /></button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('costs.undo')}>
|
||||
<button type="button" aria-label={t('costs.undo')} onClick={() => undoSettlement(s.id)} className="exp-action-danger transition-colors" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, borderRadius: 999, border: 0, cursor: 'pointer', padding: 0, color: '#dc2626' }}><RotateCcw size={13} /></button>
|
||||
<button type="button" aria-label={`${t('costs.undo')}: ${who}`} onClick={() => undoSettlement(s.id)} className="exp-action-danger transition-colors" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, borderRadius: 999, border: 0, cursor: 'pointer', padding: 0, color: '#dc2626' }}><RotateCcw size={13} /></button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
@@ -935,7 +938,6 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
|
||||
<span className="text-content" style={{ fontWeight: 600, whiteSpace: 'nowrap' }}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
const transferLabel = (fromId: number, toId: number) => `${personName(fromId)} → ${toId === me ? t('costs.youLower') : personName(toId)}`
|
||||
// Capped and scrollable: a long trip's expense list would otherwise push the
|
||||
// rest of the sidebar off the screen every time a name is tapped.
|
||||
const listCls = { display: 'flex', flexDirection: 'column', gap: 5, marginTop: 6, maxHeight: 180, overflowY: 'auto' } as const
|
||||
|
||||
@@ -100,6 +100,9 @@ describe('JourneyShareSection', () => {
|
||||
expect(timeline.className).toContain('bg-zinc-900')
|
||||
expect(screen.getByRole('button', { name: 'Map' }).className).toContain('bg-zinc-900')
|
||||
expect(screen.getByRole('button', { name: 'Gallery' }).className).not.toContain('bg-zinc-900')
|
||||
// The state is said, not only shown (#2604).
|
||||
expect(timeline).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(screen.getByRole('button', { name: 'Gallery' })).toHaveAttribute('aria-pressed', 'false')
|
||||
expect(screen.getByText(`${window.location.origin}/public/journey/tok-abc`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function JourneyShareSection({ journeyId }: { journeyId: number }
|
||||
].map(({ key, label, icon: Icon }) => (
|
||||
<button type="button"
|
||||
key={key}
|
||||
aria-pressed={!!link[key]}
|
||||
onClick={() => togglePerm(key)}
|
||||
className={`flex items-center gap-2.5 px-3 py-2 rounded-xl border text-[12px] font-medium transition-all ${
|
||||
link[key]
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('BookingCostsSection', () => {
|
||||
const user = userEvent.setup();
|
||||
renderSection();
|
||||
await user.click(linkSelect());
|
||||
await user.type(screen.getByPlaceholderText('...'), 'snack');
|
||||
await user.type(screen.getByPlaceholderText('Search'), 'snack');
|
||||
expect(screen.getByRole('button', { name: /Snacks/ })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Souvenirs/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -298,6 +298,9 @@ describe('TripMembersModal', () => {
|
||||
const input = screen.getByDisplayValue(/\/shared\/abc123/);
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
// Each toggle says whether it is on, not only by its colour (#2604).
|
||||
expect(screen.getByRole('button', { name: 'Bookings' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Costs' })).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-019: copy share link calls clipboard.writeText', async () => {
|
||||
@@ -1095,6 +1098,19 @@ describe('TripMembersModal on a phone', () => {
|
||||
Object.defineProperty(window, 'innerWidth', { value: width, configurable: true, writable: true });
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-058b: the public link toggles say whether they are on (#2604)', async () => {
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, user_id: ownerUser.id }) });
|
||||
server.use(http.get('/api/trips/1/share-link', () => HttpResponse.json({
|
||||
token: 'abc123', share_map: true, share_bookings: true, share_packing: false, share_budget: false, share_collab: false,
|
||||
})));
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Bookings' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Costs' })).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(screen.getByRole('button', { name: 'Map & Plan' })).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-058: keeps the sheet it had, with "(you)" beside the name', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
expect(await screen.findByText(/\(you\)/i)).toBeInTheDocument();
|
||||
|
||||
@@ -24,7 +24,7 @@ function ShareLinkSection({ tripId, t }: { tripId: number; t: (key: string, para
|
||||
{/* Permission checkboxes */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
||||
{[...SHARE_SECTIONS, ...SHARE_OPTIONS].map(opt => ({ ...opt, label: t(opt.label), always: 'always' in opt && !!opt.always })).map(opt => (
|
||||
<button type="button" key={opt.key} onClick={() => !opt.always && handleUpdatePerms(opt.key, !perms[opt.key])}
|
||||
<button type="button" key={opt.key} aria-pressed={!!perms[opt.key]} aria-disabled={opt.always || undefined} onClick={() => !opt.always && handleUpdatePerms(opt.key, !perms[opt.key])}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 20,
|
||||
border: '1.5px solid', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, cursor: opt.always ? 'default' : 'pointer',
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('CustomSelect', () => {
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} searchable={true} />);
|
||||
await user.click(screen.getByRole('button')); // open
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('...');
|
||||
const searchInput = screen.getByPlaceholderText('common.search');
|
||||
await user.type(searchInput, 'ban');
|
||||
|
||||
// Only Banana should remain, Apple and Cherry should be filtered out
|
||||
@@ -257,8 +257,8 @@ describe('CustomSelect', () => {
|
||||
render(<CustomSelect ariaLabel="Choose fruit" value="" onChange={onChange} options={OPTIONS} searchable />);
|
||||
const trigger = screen.getByRole('button', { name: 'Choose fruit' });
|
||||
await user.click(trigger);
|
||||
expect(screen.getByPlaceholderText('...')).toHaveFocus();
|
||||
await user.type(screen.getByPlaceholderText('...'), 'ban');
|
||||
expect(screen.getByPlaceholderText('common.search')).toHaveFocus();
|
||||
await user.type(screen.getByPlaceholderText('common.search'), 'ban');
|
||||
await user.keyboard('{ArrowDown}{Enter}');
|
||||
expect(onChange).toHaveBeenCalledWith('banana');
|
||||
expect(trigger).toHaveFocus();
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useId } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronDown, Check } from 'lucide-react'
|
||||
import { useAnchoredPosition, scrollAnchorIntoView } from '../../hooks/useAnchoredPosition'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
interface SelectOption {
|
||||
// Callers use both string keys and numeric ids (e.g. day/place ids) as values, and
|
||||
@@ -91,6 +92,7 @@ export default function CustomSelect({
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const keyboardDirection = useRef<'first' | 'last' | null>(null)
|
||||
const menuId = useId()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Follows the trigger while the sheet scrolls and while the on-screen keyboard
|
||||
// resizes the viewport, instead of freezing at the rect measured on open (#1999).
|
||||
@@ -258,7 +260,8 @@ export default function CustomSelect({
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="..."
|
||||
placeholder={t('common.search')}
|
||||
aria-label={t('common.search')}
|
||||
style={{
|
||||
width: '100%', border: '1px solid var(--border-secondary)', borderRadius: 6,
|
||||
padding: '5px 8px', fontSize: 'calc(12px * var(--fs-scale-body, 1))', outline: 'none', fontFamily: 'inherit',
|
||||
|
||||
@@ -647,10 +647,10 @@ function ExpenseRow({ item, ctx, base, locale, t, canEdit, onEdit, onDelete, onT
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex flex-none flex-col gap-1 rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[5px]">
|
||||
<button type="button" onClick={onEdit} aria-label={t('common.edit')} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<button type="button" onClick={onEdit} aria-label={`${t('common.edit')}: ${item.name}`} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<Pencil size={12} strokeWidth={2} />
|
||||
</button>
|
||||
<button type="button" onClick={onDelete} aria-label={t('common.delete')} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<button type="button" onClick={onDelete} aria-label={`${t('common.delete')}: ${item.name}`} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<Trash2 size={12} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -707,6 +707,7 @@ function PaymentRow({ settlement, ctx, base, locale, t, personName, canEdit, onE
|
||||
// A transfer without a currency was entered in the display currency, not the trip's.
|
||||
const amount = paymentAmount(settlement, ctx)
|
||||
const line = paymentLineOf(settlement, ctx, amount)
|
||||
const who = `${personName(settlement.from_user_id)} → ${personName(settlement.to_user_id)}`
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-[6px]">
|
||||
<div className="relative min-w-0 flex-1 rounded-2xl border border-[color:var(--m-rowbr)] bg-m-card px-3 py-[12px]">
|
||||
@@ -716,7 +717,7 @@ function PaymentRow({ settlement, ctx, base, locale, t, personName, canEdit, onE
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[0.8125rem] font-bold text-m-ink">{t('costs.payment')}</div>
|
||||
<div className="truncate font-geist text-[0.65625rem] text-m-faint">{personName(settlement.from_user_id)} → {personName(settlement.to_user_id)}</div>
|
||||
<div className="truncate font-geist text-[0.65625rem] text-m-faint">{who}</div>
|
||||
{line && (
|
||||
<div className="mt-[1px] truncate font-geist text-[0.59375rem] text-m-faint">
|
||||
{formatMoney(line.entered.amount, line.entered.currency, locale)} {'→'} {formatMoney(line.into.amount, line.into.currency, locale)}
|
||||
@@ -732,10 +733,10 @@ function PaymentRow({ settlement, ctx, base, locale, t, personName, canEdit, onE
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex flex-none flex-col gap-1 rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[5px]">
|
||||
<button type="button" onClick={onEdit} aria-label={t('common.edit')} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<button type="button" onClick={onEdit} aria-label={`${t('common.edit')}: ${who}`} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<Pencil size={12} strokeWidth={2} />
|
||||
</button>
|
||||
<button type="button" onClick={onUndo} aria-label={t('costs.undo')} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<button type="button" onClick={onUndo} aria-label={`${t('costs.undo')}: ${who}`} className="flex h-[26px] w-[26px] items-center justify-center rounded-full text-m-muted">
|
||||
<RotateCcw size={12} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('MCostSheet', () => {
|
||||
expect(screen.queryByText(/live rate/)).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'EUR €' }))
|
||||
fireEvent.change(screen.getByPlaceholderText('...'), { target: { value: 'USD' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('Search'), { target: { value: 'USD' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'USD $' }))
|
||||
|
||||
expect(screen.getByText('$100.00')).toBeInTheDocument()
|
||||
|
||||
@@ -381,10 +381,16 @@ describe('MCostsTab', () => {
|
||||
expect(dayTrigger).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('FE-MOB-COSTT-024b: edit and delete on a row name the expense (#2604)', async () => {
|
||||
await renderTab()
|
||||
expect(within(rowOf('Ramen')).getByRole('button', { name: 'common.edit: Ramen' })).toBeInTheDocument()
|
||||
expect(within(rowOf('Ramen')).getByRole('button', { name: 'common.delete: Ramen' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-MOB-COSTT-024: opens the edit sheet from a row and reloads on save', async () => {
|
||||
const p = planner()
|
||||
await renderTab(p)
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: 'common.edit' }))
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: /^common\.edit(:|$)/ }))
|
||||
expect(screen.getByText('sheet editing 11')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('sheet save'))
|
||||
@@ -396,7 +402,7 @@ describe('MCostsTab', () => {
|
||||
it('FE-MOB-COSTT-025: deletes an expense after confirming and refreshes the settlement', async () => {
|
||||
const p = planner()
|
||||
await renderTab(p)
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: 'common.delete' }))
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: /^common\.delete(:|$)/ }))
|
||||
const dialog = screen.getByRole('dialog', { name: 'costs.confirm.deleteTitle' })
|
||||
expect(within(dialog).getByText('costs.confirm.deleteBody:Ramen')).toBeInTheDocument()
|
||||
fireEvent.click(within(dialog).getByText('common.delete'))
|
||||
@@ -408,7 +414,7 @@ describe('MCostsTab', () => {
|
||||
const p = planner()
|
||||
vi.mocked(p.tripActions.deleteBudgetItem).mockRejectedValueOnce(new Error('offline'))
|
||||
await renderTab(p)
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: 'common.delete' }))
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: /^common\.delete(:|$)/ }))
|
||||
fireEvent.click(within(screen.getByRole('dialog')).getByText('common.delete'))
|
||||
await waitFor(() => expect(p.toast.error).toHaveBeenCalledWith('common.unknownError'))
|
||||
})
|
||||
@@ -500,8 +506,8 @@ describe('MCostsTab', () => {
|
||||
it('FE-MOB-COSTT-032: a read-only member gets no add, edit or delete affordances', async () => {
|
||||
await renderTab(planner({ can: vi.fn(() => false) as unknown as TripPlanner['can'] }))
|
||||
expect(screen.queryByRole('button', { name: 'costs.addPayment' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.edit' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.delete' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^common\.edit(:|$)/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^common\.delete(:|$)/ })).not.toBeInTheDocument()
|
||||
expect(within(cardOf('Ramen')).getByRole('button', { pressed: false })).toBeDisabled()
|
||||
})
|
||||
|
||||
@@ -533,7 +539,7 @@ describe('MCostsTab', () => {
|
||||
it('FE-MOB-COSTT-035: cancelling the confirm keeps the expense', async () => {
|
||||
const p = planner()
|
||||
await renderTab(p)
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: 'common.delete' }))
|
||||
fireEvent.click(within(rowOf('Ramen')).getByRole('button', { name: /^common\.delete(:|$)/ }))
|
||||
fireEvent.click(within(screen.getByRole('dialog')).getByText('common.cancel'))
|
||||
expect(p.tripActions.deleteBudgetItem).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Ramen')).toBeInTheDocument()
|
||||
@@ -682,7 +688,7 @@ describe('MCostsTab', () => {
|
||||
const update = vi.spyOn(budgetApi, 'updateSettlement').mockResolvedValue({})
|
||||
await renderTab()
|
||||
|
||||
fireEvent.click(within(rowOf('costs.payment')).getByRole('button', { name: 'common.edit' }))
|
||||
fireEvent.click(within(rowOf('costs.payment')).getByRole('button', { name: /^common\.edit(:|$)/ }))
|
||||
const dialog = screen.getByRole('dialog', { name: 'costs.editPayment' })
|
||||
expect(within(dialog).getByDisplayValue('25.00')).toBeInTheDocument()
|
||||
|
||||
@@ -700,7 +706,7 @@ describe('MCostsTab', () => {
|
||||
const del = vi.spyOn(budgetApi, 'deleteSettlement').mockResolvedValue({ success: true })
|
||||
await renderTab()
|
||||
|
||||
fireEvent.click(within(rowOf('costs.payment')).getByRole('button', { name: 'costs.undo' }))
|
||||
fireEvent.click(within(rowOf('costs.payment')).getByRole('button', { name: /^costs\.undo(:|$)/ }))
|
||||
expect(del).toHaveBeenCalledWith(7, 502)
|
||||
await waitFor(() => expect(settlementBases).toHaveLength(2))
|
||||
})
|
||||
@@ -718,7 +724,7 @@ describe('MCostsTab', () => {
|
||||
expect(within(row).getByText('£10.00 → $20.00')).toBeInTheDocument()
|
||||
expect(within(row).getByText('$20.00')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(within(row).getByRole('button', { name: 'common.edit' }))
|
||||
fireEvent.click(within(row).getByRole('button', { name: /^common\.edit(:|$)/ }))
|
||||
const dialog = screen.getByRole('dialog', { name: 'costs.editPayment' })
|
||||
expect(within(dialog).getByDisplayValue('10.00')).toBeInTheDocument()
|
||||
expect(within(dialog).getByRole('button', { name: /GBP/ })).toBeInTheDocument()
|
||||
|
||||
@@ -42,7 +42,7 @@ const budget: TranslationStrings = {
|
||||
'budget.categoriesLabel': 'カテゴリ',
|
||||
'costs.you': '自分',
|
||||
'costs.youShort': 'Y',
|
||||
'costs.youLower': 'you',
|
||||
'costs.youLower': '自分',
|
||||
'costs.youOwe': '支払う額',
|
||||
'costs.youOweSub': '他の人に支払うべき額',
|
||||
'costs.youreOwed': '受け取る額',
|
||||
|
||||
Reference in New Issue
Block a user