import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import ExperimentTimeline from '../../components/ExperimentTimeline.vue' import type { ProtocolStep } from '../../types' function makeSteps(): ProtocolStep[] { return [ { id: 's1', name: 'Thaw', type: 'incubation', status: 'pending', order: 0 }, { id: 's2', name: 'Quench', type: 'incubation', status: 'pending', order: 1 }, { id: 's3', name: 'Extract', type: 'incubation', status: 'pending', order: 2 }, ] } describe('ExperimentTimeline', () => { it('renders steps in order', () => { const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps() } }) expect(wrapper.findAll('.mint-timeline__title').map(title => title.text())) .toEqual(['Thaw', 'Quench', 'Extract']) }) it('reorders a step with Alt and the arrow keys while editable', async () => { const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps(), editable: true }, }) const first = wrapper.findAll('.mint-timeline__step')[0] expect(first.attributes('tabindex')).toBe('0') expect(first.attributes('aria-label')).toContain('Press Alt with the arrow keys') await first.trigger('keydown', { key: 'ArrowDown', altKey: true }) expect(wrapper.emitted('step-reorder')?.[0]).toEqual(['s1', 1]) const steps = wrapper.emitted('update:modelValue')?.[0]?.[0] as ProtocolStep[] expect(steps.map(step => step.id)).toEqual(['s2', 's1', 's3']) expect(steps.map(step => step.order)).toEqual([0, 1, 2]) }) it('announces the move politely', async () => { const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps(), editable: true }, }) await wrapper.findAll('.mint-timeline__step')[2] .trigger('keydown', { key: 'ArrowUp', altKey: true }) await wrapper.vm.$nextTick() const liveRegion = wrapper.get('[role="status"]') expect(liveRegion.attributes('aria-live')).toBe('polite') expect(liveRegion.text()).toBe('Extract moved to position 2 of 3.') }) it('is not keyboard reorderable when not editable', async () => { const wrapper = mount(ExperimentTimeline, { props: { modelValue: makeSteps() } }) const first = wrapper.findAll('.mint-timeline__step')[0] expect(first.attributes('tabindex')).toBeUndefined() await first.trigger('keydown', { key: 'ArrowDown', altKey: true }) expect(wrapper.emitted('step-reorder')).toBeUndefined() }) describe('step progress (3d)', () => { function mountWith(step: Partial) { const steps: ProtocolStep[] = [ { id: 'run', name: 'LC-MS', type: 'incubation', status: 'in_progress', order: 0, ...step }, ] return mount(ExperimentTimeline, { props: { modelValue: steps } }) } it('should render a progress bar when the in-progress step has progress', () => { expect(mountWith({ progress: 35 }).find('[role="progressbar"]').exists()).toBe(true) }) it('should expose the value on the progress bar when the step has progress', () => { expect(mountWith({ progress: 35 }).find('[role="progressbar"]').attributes('aria-valuenow')).toBe('35') }) it('should size the fill to the percent when the step has progress', () => { expect(mountWith({ progress: 35 }).find('.mint-timeline__progress-fill').attributes('style')).toContain('width: 35%') }) it('should show the percent as text when the step has progress', () => { expect(mountWith({ progress: 35 }).find('.mint-timeline__progress-value').text()).toBe('35%') }) it('should round the percent text when progress is fractional', () => { expect(mountWith({ progress: 34.6 }).find('.mint-timeline__progress-value').text()).toBe('35%') }) it('should clamp the percent when progress exceeds 100', () => { expect(mountWith({ progress: 140 }).find('.mint-timeline__progress-value').text()).toBe('100%') }) it('should clamp the percent when progress is negative', () => { expect(mountWith({ progress: -5 }).find('.mint-timeline__progress-value').text()).toBe('0%') }) it('should render no progress bar when the in-progress step has no progress', () => { expect(mountWith({}).find('[role="progressbar"]').exists()).toBe(false) }) it('should render no progress bar when the step is not in progress', () => { expect(mountWith({ status: 'completed', progress: 100 }).find('[role="progressbar"]').exists()).toBe(false) }) }) })