import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import SmartGroupFieldRecipe from '../../components/SmartGroupFieldRecipe.vue' import type { SmartGroupFieldRecipeProps } from '../../components/SmartGroup.types' const fields = [ { key: 'Sample ID', role: 'Sample ID', vals: '212, 213, 214 … +7', rep: true }, { key: 'Genotype', role: 'Factor', vals: 'WT, KI', rep: false }, { key: 'Dose', role: 'Factor', vals: '3, 6', rep: false }, ] function mountControlled(extra: Partial = {}) { return mount(SmartGroupFieldRecipe, { props: { fields, enabled: [false, true, true], layers: [null, 2, 1], groups: [{ name: 'WT / 3', count: 3, color: '#3b82f6' }], route: 'Overlay only', headerCount: 15, qcCount: 5, experimentLabel: '', ...extra, }, attachTo: document.body, }) } describe('SmartGroupFieldRecipe', () => { it('should draw the QC chip dot as the neutral QC marker when a qcColor is set', () => { const wrapper = mount(SmartGroupFieldRecipe, { props: { qcColor: '#f43f5e' } }) const dot = wrapper.find('.sg-qc__chip .sg-dot').element as HTMLElement expect(dot.style.borderColor).toBe('var(--text-secondary-strong)') }) it('should list the grouped fields in layer order when layers are given', () => { const names = mountControlled().findAll('[data-test="field-on"] .sg-field__name').map(node => node.text()) expect(names).toEqual(['Dose', 'Genotype']) }) it('should list a field under "Left off" when it is not grouped by', () => { const names = mountControlled().findAll('[data-test="field-off"] .sg-field__name').map(node => node.text()) expect(names).toEqual(['Sample ID']) }) it('should emit toggle with the card index when the user switches a field', async () => { const wrapper = mountControlled() await wrapper.find('[data-test="field-off"] [role="switch"]').trigger('click') expect(wrapper.emitted('toggle')).toEqual([[0]]) }) it('should emit move up when the user presses Alt+ArrowUp on the handle of the second layer', async () => { const wrapper = mountControlled() await wrapper.findAll('[data-test="field-on"] .fr-row__grip')[1].trigger('keydown', { key: 'ArrowUp', altKey: true }) expect(wrapper.emitted('move')).toEqual([[1, 'up']]) }) it('should emit rename with the new name when the user commits the name input', async () => { const wrapper = mountControlled({ renamable: true }) await wrapper.find('[data-test="field-on"] .fr-row__rename').trigger('click') const input = wrapper.find('[data-test="field-on"] input') await input.setValue('Tracer dose') await input.trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('rename')).toEqual([[2, 'Tracer dose']]) }) it('should show no rename control when the fields are not renamable', () => { expect(mountControlled().find('.fr-row__rename').exists()).toBe(false) }) it('should show the hint of a field when hints are given', () => { const wrapper = mountControlled({ hints: [null, 'off → 2 groups', null] }) expect(wrapper.findAll('[data-test="field-on"]')[1].find('.fr-row__hint').text()).toBe('off → 2 groups') }) it('should show the tokens of the example name when the anatomy is given', () => { const wrapper = mountControlled({ anatomy: [ { text: '212', caption: 'Sample ID', state: 'off' }, { text: 'WT', caption: '2', state: 'on' }, ], }) expect(wrapper.findAll('.fr-token__text').map(node => node.text())).toEqual(['212', 'WT']) }) it('should show the empty state when there are no fields', () => { const wrapper = mountControlled({ fields: [], enabled: [], layers: [], groups: [], headerCount: 0 }) expect(wrapper.find('[data-test="recipe-empty"]').exists()).toBe(true) }) it('should show one chip for each QC class when qcChips are given', () => { const wrapper = mountControlled({ qcChips: [{ name: 'EQC', count: 3 }, { name: 'Blank', count: 2 }] }) expect(wrapper.findAll('.sg-qc__chip').map(node => node.text())).toEqual(['EQC ×3', 'Blank ×2']) }) it('should emit update:route when the user picks another QC routing', async () => { const wrapper = mountControlled() const exclude = wrapper.findAll('.sg-qc [role="radio"]').find(node => node.text() === 'Exclude') await exclude!.trigger('click') expect(wrapper.emitted('update:route')).toEqual([['Exclude']]) }) it('should warn that every group has one sample when the groups are all singletons', () => { const wrapper = mountControlled({ groups: [{ name: 'a', count: 1, color: '#000' }, { name: 'b', count: 1, color: '#000' }], }) expect(wrapper.find('.sg-summary--warn').text()).toContain('one sample') }) }) describe('SmartGroupFieldRecipe keyboard and edge cases', () => { const switchOf = (wrapper: ReturnType, name: string) => wrapper.findAll('.fr-row').find(row => row.find('.sg-field__name').text() === name)!.find('[role="switch"]') it('should keep focus on the switch of a field when the field moves to the other list', async () => { const wrapper = mount(SmartGroupFieldRecipe, { attachTo: document.body }) await switchOf(wrapper, 'Dose').trigger('click') await new Promise(resolve => setTimeout(resolve)) expect(document.activeElement).toBe(switchOf(wrapper, 'Dose').element) }) it('should keep focus on the handle when a field moves up with the keyboard', async () => { const wrapper = mount(SmartGroupFieldRecipe, { attachTo: document.body }) const grip = wrapper.findAll('[data-test="field-on"] .fr-row__grip')[1] ;(grip.element as HTMLElement).focus() await grip.trigger('keydown', { key: 'ArrowUp', altKey: true }) await new Promise(resolve => setTimeout(resolve)) expect(document.activeElement).toBe(wrapper.findAll('[data-test="field-on"] .fr-row__grip')[0].element) }) it('should keep the name input open and emit nothing when another field has the new name', async () => { const wrapper = mountControlled({ renamable: true }) await wrapper.find('[data-test="field-on"] .fr-row__rename').trigger('click') const input = wrapper.find('[data-test="field-on"] input') await input.setValue('Genotype') await input.trigger('keydown', { key: 'Enter' }) expect([wrapper.emitted('rename'), wrapper.find('[data-test="field-on"] input').attributes('aria-invalid')]).toEqual([undefined, 'true']) }) it('should not let the row be dragged when its name is being edited', async () => { const wrapper = mountControlled({ renamable: true }) await wrapper.find('[data-test="field-on"] .fr-row__rename').trigger('click') expect(wrapper.find('[data-test="field-on"]').attributes('draggable')).toBe('false') }) it('should not show the empty state when the batch has samples but no field', () => { const wrapper = mountControlled({ fields: [], enabled: [], layers: [], groups: [], headerCount: 2 }) expect(wrapper.find('[data-test="recipe-empty"]').exists()).toBe(false) }) it('should move a field to "Left off" when layers are given but the toggles are not controlled', async () => { const wrapper = mount(SmartGroupFieldRecipe, { props: { fields, layers: [null, 2, 1] }, attachTo: document.body }) await switchOf(wrapper, 'Dose').trigger('click') const names = wrapper.findAll('[data-test="field-on"] .sg-field__name').map(node => node.text()) expect(names).toEqual(['Genotype']) }) }) describe('SmartGroupFieldRecipe second review', () => { const twoFields = [ { key: 'A', role: 'Factor', vals: 'a1, a2', rep: false }, { key: 'B', role: 'Factor', vals: 'b1, b2', rep: false }, ] const onNames = (wrapper: ReturnType) => wrapper.findAll('[data-test="field-on"] .sg-field__name').map(node => node.text()) it('should reorder the rows when the parent updates layers and the toggles are not controlled', async () => { const wrapper = mount(SmartGroupFieldRecipe, { props: { fields: twoFields, layers: [1, 2] } }) await wrapper.setProps({ layers: [2, 1] }) expect(onNames(wrapper)).toEqual(['B', 'A']) }) it('should emit move when layers are given and the toggles are not controlled', async () => { const wrapper = mount(SmartGroupFieldRecipe, { props: { fields: twoFields, layers: [1, 2] } }) await wrapper.findAll('.fr-row__grip')[1].trigger('keydown', { key: 'ArrowUp', altKey: true }) expect(wrapper.emitted('move')).toEqual([[1, 'up']]) }) it('should keep the name input open when the new name is a reserved name', async () => { const wrapper = mountControlled({ renamable: true, reservedNames: ['Matrix'] }) await wrapper.find('[data-test="field-on"] .fr-row__rename').trigger('click') const input = wrapper.find('[data-test="field-on"] input') await input.setValue('Matrix') await input.trigger('keydown', { key: 'Enter' }) expect([wrapper.emitted('rename'), wrapper.find('[data-test="field-on"] input').attributes('aria-invalid')]).toEqual([undefined, 'true']) }) })