chorus/frontend/tests/KanbanBoard.spec.js
pyr0ball 575fb3aaeb fix: add arrow-key roving tabindex nav and aria-pressed to triage views
KanbanBoard's board tabs implemented roving tabindex but never wired up
arrow-key navigation, making inactive tabs keyboard-unreachable. Adds
ArrowLeft/ArrowRight/Home/End handling that moves both selection and
focus between tabs, closing an unmet requirement from the original
design doc. Also adds aria-pressed to App's List/Board toggle buttons
so their active state is exposed to assistive technology, not just
conveyed visually via CSS class.
2026-07-14 17:51:43 -07:00

107 lines
4.6 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import KanbanBoard from '../src/components/KanbanBoard.vue'
function makeItem(overrides = {}) {
return {
id: 1,
modality: 'bh_email',
sender_id: 'jane@example.com',
raw_content: 'I have some books to donate.',
type: null,
stage: 'new',
direction: 'inbound',
follow_up_date: '',
...overrides,
}
}
describe('KanbanBoard', () => {
it('buckets items into the correct board and shows counts on tabs', () => {
const items = [
makeItem({ id: 1, type: 'donation', direction: 'inbound', stage: 'new' }),
makeItem({ id: 2, type: 'other', direction: 'inbound', stage: 'new' }),
makeItem({ id: 3, type: null, direction: 'outbound', stage: 'new' }),
makeItem({ id: 4, type: null, direction: 'inbound', stage: 'new' }),
]
const wrapper = mount(KanbanBoard, { props: { items } })
const tabs = wrapper.findAll('[role="tab"]').map((t) => t.text())
expect(tabs).toEqual([
'Donations (1)', 'Other (1)', 'Outbound Leads (1)', 'Unsorted (1)',
])
})
it('defaults to the Donations board and its first stage', () => {
const wrapper = mount(KanbanBoard, { props: { items: [] } })
const activeTab = wrapper.find('[role="tab"][aria-selected="true"]')
expect(activeTab.text()).toContain('Donations')
})
it('shows humanized stage labels, not raw stage strings, on column headers', () => {
const items = [makeItem({ id: 1, type: 'donation', stage: 'pickup_scheduled' })]
const wrapper = mount(KanbanBoard, { props: { items } })
expect(wrapper.text()).toContain('Pickup Scheduled')
expect(wrapper.text()).not.toContain('pickup_scheduled')
})
it('switching board tabs resets the active stage to that board\'s first stage', async () => {
const wrapper = mount(KanbanBoard, { props: { items: [] } })
const outboundTab = wrapper.findAll('[role="tab"]').find((t) => t.text().includes('Outbound Leads'))
await outboundTab.trigger('click')
const breadcrumb = wrapper.find('.mobile-breadcrumb')
expect(breadcrumb.text()).toContain('New')
expect(breadcrumb.text()).toContain('Outbound Leads')
})
it('clicking a stage-strip button switches the active stage', async () => {
const items = [makeItem({ id: 1, type: 'donation', stage: 'replied' })]
const wrapper = mount(KanbanBoard, { props: { items } })
const repliedButton = wrapper.findAll('.stage-strip-button').find((b) => b.text().includes('Replied'))
await repliedButton.trigger('click')
expect(wrapper.find('.mobile-breadcrumb').text()).toContain('Replied')
})
it('clicking a card emits select-item with the item id', async () => {
const items = [makeItem({ id: 42, type: 'donation', stage: 'new' })]
const wrapper = mount(KanbanBoard, { props: { items } })
await wrapper.find('.card').trigger('click')
expect(wrapper.emitted('select-item')).toEqual([[42]])
})
it('renders an empty-state message for a column with no items', () => {
const wrapper = mount(KanbanBoard, { props: { items: [] } })
expect(wrapper.text()).toContain('Nothing here')
})
it('applies the is-active-mobile class only to the currently active stage column', () => {
const items = [
makeItem({ id: 1, type: 'donation', stage: 'new' }),
makeItem({ id: 2, type: 'donation', stage: 'replied' }),
]
const wrapper = mount(KanbanBoard, { props: { items } })
const columns = wrapper.findAll('.column')
const activeColumns = columns.filter((c) => c.classes().includes('is-active-mobile'))
expect(activeColumns).toHaveLength(1)
expect(activeColumns[0].text()).toContain('New')
})
it('ArrowRight on a board tab moves selection and focus to the next tab', async () => {
const wrapper = mount(KanbanBoard, { props: { items: [] }, attachTo: document.body })
const tabs = wrapper.findAll('[role="tab"]')
tabs[0].element.focus()
await tabs[0].trigger('keydown', { key: 'ArrowRight' })
const updatedTabs = wrapper.findAll('[role="tab"]')
expect(updatedTabs[1].attributes('aria-selected')).toBe('true')
expect(document.activeElement).toBe(updatedTabs[1].element)
wrapper.unmount()
})
it('ArrowLeft wraps from the first tab to the last tab', async () => {
const wrapper = mount(KanbanBoard, { props: { items: [] }, attachTo: document.body })
const tabs = wrapper.findAll('[role="tab"]')
await tabs[0].trigger('keydown', { key: 'ArrowLeft' })
const updatedTabs = wrapper.findAll('[role="tab"]')
expect(updatedTabs[3].attributes('aria-selected')).toBe('true')
wrapper.unmount()
})
})