import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import ReagentList from '../../components/ReagentList.vue' import type { Reagent } from '../../types' describe('ReagentList', () => { const reagents: Reagent[] = [ { id: 'reagent-1', name: 'Glucose', catalogNumber: 'G-7528', lotNumber: 'LOT-A', supplier: 'Sigma', }, { id: 'reagent-2', name: 'Lactate', catalogNumber: 'L-1250', lotNumber: 'LOT-B', supplier: 'Cayman', }, ] it('searches reagent metadata case-insensitively', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, readonly: true, columns: ['name', 'catalog', 'lot', 'supplier'], }, }) await wrapper.find('.mint-reagent-list__search-input').setValue(' cay ') const rows = wrapper.findAll('tbody tr') expect(rows).toHaveLength(1) expect(rows[0].text()).toContain('Lactate') }) it('treats whitespace-only search as empty', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, readonly: true, columns: ['name', 'catalog', 'lot', 'supplier'], }, }) await wrapper.find('.mint-reagent-list__search-input').setValue(' ') expect(wrapper.findAll('tbody tr')).toHaveLength(2) }) it('sorts reagent columns while keeping missing values last', async () => { const wrapper = mount(ReagentList, { props: { modelValue: [ ...reagents, { id: 'reagent-3', name: 'Pyruvate' }, ], readonly: true, columns: ['name', 'catalog'], }, }) await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click') const rows = wrapper.findAll('tbody tr') expect(rows.map(row => row.text())).toEqual([ expect.stringContaining('Glucose'), expect.stringContaining('Lactate'), expect.stringContaining('Pyruvate'), ]) await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click') expect(wrapper.findAll('tbody tr').map(row => row.text())).toEqual([ expect.stringContaining('Lactate'), expect.stringContaining('Glucose'), expect.stringContaining('Pyruvate'), ]) }) it('exposes sortable headers as buttons that sort from the keyboard', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, readonly: true, columns: ['name', 'catalog'] }, }) const nameHeader = () => wrapper.findAll('th')[0] expect(nameHeader().attributes('aria-sort')).toBe('none') expect(nameHeader().get('.mint-reagent-list__sort-btn').element.tagName).toBe('BUTTON') await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: 'Enter' }) expect(nameHeader().attributes('aria-sort')).toBe('ascending') await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: ' ' }) expect(nameHeader().attributes('aria-sort')).toBe('descending') }) it('omits aria-sort when sorting is disabled', () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, readonly: true, sortable: false, columns: ['name'] }, }) expect(wrapper.findAll('th')[0].attributes('aria-sort')).toBeUndefined() }) it('reorders reagents from the drag handle with Alt and the arrow keys', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, columns: ['name'] }, }) const handle = wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0] expect(handle.attributes('aria-label')).toContain('Reorder Glucose') await handle.trigger('keydown', { key: 'ArrowDown', altKey: true }) expect(wrapper.emitted('reorder')?.[0]).toEqual(['reagent-1', 0, 1]) expect( (wrapper.emitted('update:modelValue')?.[0]?.[0] as Reagent[]).map(reagent => reagent.id), ).toEqual(['reagent-2', 'reagent-1']) }) it('does not reorder from the keyboard while readonly', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, readonly: true, columns: ['name'] }, }) expect(wrapper.find('.mint-reagent-list__drag-handle-btn').exists()).toBe(false) }) it('announces a keyboard reorder in a polite live region', async () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents, columns: ['name'] }, }) await wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0] .trigger('keydown', { key: 'ArrowDown', altKey: true }) await wrapper.vm.$nextTick() const liveRegion = wrapper.get('[role="status"]') expect(liveRegion.attributes('aria-live')).toBe('polite') expect(liveRegion.text()).toBe('Glucose moved to position 2 of 2.') }) it('should mark low stock with a dot and text when stock is under the threshold', () => { const wrapper = mount(ReagentList, { props: { modelValue: [{ id: 'r', name: 'Acetonitrile', stockLevel: 8 }], readonly: true }, }) expect(wrapper.find('.mint-reagent-list__stock-text').text()).toMatch(/^Low ยท\s*8%$/) }) it('should state days left when a reagent expires soon', () => { const soon = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000 - 60_000).toISOString() const wrapper = mount(ReagentList, { props: { modelValue: [{ id: 'r', name: 'Formic acid', expiryDate: soon }], readonly: true }, }) expect(wrapper.find('.mint-reagent-list__badge--warning').text()).toBe('3 days left') }) it('should render the add action as a BaseButton when editable', () => { const wrapper = mount(ReagentList, { props: { modelValue: reagents } }) expect(wrapper.find('.mint-reagent-list__add-btn').classes()).toContain('mint-button') }) })