import { mount } from '@vue/test-utils' import { h, nextTick, ref } from 'vue' import { describe, expect, it } from 'vitest' import WellEditPopupInternal from '../../components/internal/WellEditPopupInternal.vue' import SegmentedControl from '../../components/SegmentedControl.vue' function mountPopup(attachTo?: HTMLElement) { return mount(WellEditPopupInternal, { props: { wellId: 'A1', position: { x: 10, y: 10 }, }, attachTo, global: { stubs: { teleport: true }, }, }) } describe('WellEditPopupInternal focus trap', () => { it('moves focus into the popup on mount', async () => { const host = document.createElement('div') document.body.appendChild(host) const wrapper = mountPopup(host) await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() expect(wrapper.find('.mint-well-edit-popup').element.contains(document.activeElement)).toBe(true) wrapper.unmount() host.remove() }) it('restores focus to the previously focused element on unmount', async () => { const host = document.createElement('div') document.body.appendChild(host) const trigger = document.createElement('button') host.appendChild(trigger) trigger.focus() expect(document.activeElement).toBe(trigger) const wrapper = mountPopup(host) await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() expect(document.activeElement).not.toBe(trigger) wrapper.unmount() expect(document.activeElement).toBe(trigger) host.remove() }) it('wraps Tab from the last focusable element back to the first', async () => { const host = document.createElement('div') document.body.appendChild(host) const wrapper = mountPopup(host) await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() const focusable = (wrapper.element as HTMLElement).querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', ) expect(focusable.length).toBeGreaterThan(1) const last = focusable[focusable.length - 1] const first = focusable[0] last.focus() const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }) document.dispatchEvent(event) expect(document.activeElement).toBe(first) wrapper.unmount() host.remove() }) it('wraps Shift+Tab from the first focusable element back to the last', async () => { const host = document.createElement('div') document.body.appendChild(host) const wrapper = mountPopup(host) await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() const focusable = (wrapper.element as HTMLElement).querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', ) const first = focusable[0] const last = focusable[focusable.length - 1] first.focus() const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true, cancelable: true }) document.dispatchEvent(event) expect(document.activeElement).toBe(last) wrapper.unmount() host.remove() }) it('closes on Escape', async () => { const wrapper = mountPopup() await wrapper.vm.$nextTick() await wrapper.get('.mint-well-edit-popup').trigger('keydown', { key: 'Escape' }) expect(wrapper.emitted('close')).toHaveLength(1) wrapper.unmount() }) }) function mountWith(props: Record) { return mount(WellEditPopupInternal, { props: { wellId: 'B3', position: { x: 10, y: 10 }, ...props }, global: { stubs: { teleport: true } }, }) } function well(sampleType: string, metadata: Record = {}) { return { sampleType, state: 'filled' as const, metadata: { label: 'S-0012', ...metadata } } } describe('WellEditPopupInternal rebuilt from SDK parts (3b)', () => { it('should use the shared popover surface', () => { expect(mountWith({}).get('.mint-well-edit-popup').classes()).toContain('mint-popover') }) it('should show the QC marker glyph in the header', () => { expect(mountWith({ wellData: well('qc') }).get('.mint-well-edit-popup__marker').text()).toBe('Q') }) it('should save the type picked in the type control', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.findAll('[data-field="sampleType"] [role="radio"]')[1].trigger('click') await wrapper.get('[data-action="save"]').trigger('click') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ sampleType: 'blank' }) }) it('should save the injection count picked in the injections control', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.findAll('[data-field="injectionCount"] [role="radio"]')[2].trigger('click') await wrapper.get('[data-action="save"]').trigger('click') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ injectionCount: 3 }) }) it('should keep Advanced closed for a single injection', () => { expect(mountWith({ wellData: well('sample') }).find('.mint-well-edit-popup__advanced').exists()).toBe(false) }) it('should open Advanced when the well has more than five injections', () => { expect(mountWith({ wellData: well('sample', { injectionCount: 7 }) }).find('.mint-well-edit-popup__advanced').exists()).toBe(true) }) it('should open Advanced when the well has a custom method', () => { expect(mountWith({ wellData: well('sample', { customMethod: 'HILIC' }) }).find('.mint-well-edit-popup__advanced').exists()).toBe(true) }) it('should save on Enter in the name field', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('save')).toHaveLength(1) }) it('should emit clear from the clear button', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.get('[data-action="clear"]').trigger('click') expect(wrapper.emitted('clear')).toHaveLength(1) }) it('should leave out fields that are not listed', () => { expect(mountWith({ editFields: ['label'] }).find('[data-field="sampleType"]').exists()).toBe(false) }) it('should save the default volume when the volume field is emptied', async () => { const wrapper = mountWith({ defaultInjectionVolume: 7 }) await wrapper.get('[data-field="injectionVolume"] input').setValue('') await wrapper.get('[data-action="save"]').trigger('click') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ injectionVolume: 7 }) }) }) describe('WellEditPopupInternal field names', () => { it.each([ ['sampleType', 'Type'], ['injectionCount', 'Injections'], ])('should name the %s radio group %s', (field, name) => { const wrapper = mountPopup() expect(wrapper.get(`[data-field="${field}"] [role="radiogroup"]`).attributes('aria-label')).toBe(name) }) }) describe('WellEditPopupInternal inline (7a rail)', () => { it('should render in place without a backdrop', () => { const wrapper = mountWith({ inline: true }) expect(wrapper.find('.mint-well-edit-popup__backdrop').exists()).toBe(false) }) it('should drop the popover surface', () => { const wrapper = mountWith({ inline: true }) expect(wrapper.get('.mint-well-edit-popup').classes()).not.toContain('mint-popover') }) it('should stack field labels above their controls', () => { const wrapper = mountWith({ inline: true }) expect(wrapper.find('.mint-form-field--row').exists()).toBe(false) }) it('should focus the name field on mount', async () => { const host = document.createElement('div') document.body.appendChild(host) const wrapper = mount(WellEditPopupInternal, { props: { wellId: 'A1', position: { x: 0, y: 0 }, inline: true }, attachTo: host, }) await wrapper.vm.$nextTick() expect(document.activeElement).toBe(wrapper.get('[data-field="label"] input').element) wrapper.unmount() host.remove() }) it('should focus the name field when another well is selected', async () => { const host = document.createElement('div') document.body.appendChild(host) const well = document.createElement('div') well.tabIndex = 0 document.body.appendChild(well) const wrapper = mount(WellEditPopupInternal, { props: { wellId: 'A1', position: { x: 0, y: 0 }, inline: true }, attachTo: host, }) await wrapper.vm.$nextTick() well.focus() await wrapper.setProps({ wellId: 'A2' }) expect(document.activeElement).toBe(wrapper.get('[data-field="label"] input').element) wrapper.unmount() host.remove() well.remove() }) it('should hand focus back to the well when it closes', async () => { const host = document.createElement('div') document.body.appendChild(host) const well = document.createElement('div') well.tabIndex = 0 document.body.appendChild(well) well.focus() // Closed by v-if, as the rail does; wrapper.unmount() detaches the DOM before beforeUnmount. const open = ref(true) const wrapper = mount(() => (open.value ? h(WellEditPopupInternal, { wellId: 'A1', position: { x: 0, y: 0 }, inline: true }) : null), { attachTo: host, }) await nextTick() open.value = false await nextTick() expect(document.activeElement).toBe(well) wrapper.unmount() host.remove() well.remove() }) it('should close on Escape', async () => { const wrapper = mountWith({ inline: true }) await wrapper.get('.mint-well-edit-popup').trigger('keydown', { key: 'Escape' }) expect(wrapper.emitted('close')).toHaveLength(1) }) }) describe('WellEditPopupInternal marker color', () => { it('should tint the header marker with the color of the well type', () => { const wrapper = mountWith({ wellData: well('ctrl'), markerColors: { ctrl: 'var(--mint-sample-4)' } }) expect(wrapper.get('.mint-well-edit-popup__marker').attributes('style')).toContain('var(--mint-sample-4)') }) it('should tint the header marker with the color of the Type being chosen', async () => { const wrapper = mountWith({ wellData: well('sample'), markerColors: { sample: 'rgb(31, 119, 180)', iqc: 'rgb(214, 39, 40)' } }) await wrapper.getComponent(SegmentedControl).vm.$emit('update:modelValue', 'iqc') expect(wrapper.get('.mint-well-edit-popup__marker').attributes('style')).toContain('rgb(214, 39, 40)') }) }) describe('WellEditPopupInternal inline clicks', () => { it('should let clicks bubble to the document so open menus can close', async () => { const host = document.createElement('div') document.body.appendChild(host) const wrapper = mount(WellEditPopupInternal, { props: { wellId: 'A1', position: { x: 0, y: 0 }, inline: true }, attachTo: host }) let reached = false const listener = () => { reached = true } document.addEventListener('click', listener) await wrapper.get('.mint-well-edit-popup').trigger('click') expect(reached).toBe(true) document.removeEventListener('click', listener) wrapper.unmount() host.remove() }) }) describe('WellEditPopupInternal header (7a)', () => { it('should name the group in the header', () => { const wrapper = mountWith({ inline: true, wellData: well('sample'), groupLabel: 'Control' }) expect(wrapper.get('.mint-well-edit-popup__title').text()).toBe('Control') }) it('should tint the header marker with the group color', () => { const wrapper = mountWith({ wellData: { ...well('sample'), group: 'ctrl' }, markerColors: { ctrl: 'var(--mint-sample-4)' } }) expect(wrapper.get('.mint-well-edit-popup__marker').attributes('style')).toContain('var(--mint-sample-4)') }) }) describe('WellEditPopupInternal inline commits (7a)', () => { it('should save when a type segment is clicked', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.findAll('[data-field="sampleType"] [role="radio"]')[2].trigger('click') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ sampleType: 'qc' }) }) it('should save when an injection segment is clicked', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.findAll('[data-field="injectionCount"] [role="radio"]')[1].trigger('click') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ injectionCount: 2 }) }) it('should save a changed name when it loses focus', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const input = wrapper.get('[data-field="label"] input') await input.setValue('S-0099') await input.trigger('focusout') expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ label: 'S-0099' }) }) it('should save a changed volume on Enter', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const input = wrapper.get('[data-field="injectionVolume"] input') await input.setValue('12') await input.trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ injectionVolume: 12 }) }) it('should close on Enter in the inline name field', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('close')).toHaveLength(1) }) it('should stay open when Enter confirms an IME composition in the inline name field', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('keydown', { key: 'Enter', isComposing: true }) expect(wrapper.emitted('close')).toBeUndefined() }) it('should stay open when Safari reports the IME Enter as keyCode 229', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('keydown', { key: 'Enter', keyCode: 229 }) expect(wrapper.emitted('close')).toBeUndefined() }) it('should save a changed name before it closes on Enter', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const input = wrapper.get('[data-field="label"] input') await input.setValue('S-0099') await input.trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('save')?.[0]?.[0]).toMatchObject({ label: 'S-0099' }) }) it('should not save when focus leaves an unchanged field', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('focusout') expect(wrapper.emitted('save')).toBeUndefined() }) it('should not save when focus moves to another field inside the inline inspector', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const input = wrapper.get('[data-field="label"] input') await input.setValue('') await input.trigger('focusout', { relatedTarget: wrapper.findAll('[data-field="sampleType"] [role="radio"]')[1].element }) expect(wrapper.emitted('save')).toBeUndefined() }) it('should keep focus in the name field when a type segment is pressed', () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const press = new MouseEvent('mousedown', { bubbles: true, cancelable: true }) wrapper.findAll('[data-field="sampleType"] [role="radio"]')[1].element.dispatchEvent(press) expect(press.defaultPrevented).toBe(true) }) it('should not save when Enter confirms an IME composition in the popover name field', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.get('[data-field="label"] input').trigger('keydown', { key: 'Enter', isComposing: true }) expect(wrapper.emitted('save')).toBeUndefined() }) it('should not save on focusout in the popover', async () => { const wrapper = mountWith({ wellData: well('sample') }) const input = wrapper.get('[data-field="label"] input') await input.setValue('S-0099') await input.trigger('focusout') expect(wrapper.emitted('save')).toBeUndefined() }) it('should drop the Save button', () => { expect(mountWith({ inline: true }).find('[data-action="save"]').exists()).toBe(false) }) it('should keep the Save button in the popover', () => { expect(mountWith({}).find('[data-action="save"]').exists()).toBe(true) }) }) describe('WellEditPopupInternal inline edit state (7a)', () => { const advancedOpen = (wrapper: ReturnType) => wrapper.find('.mint-well-edit-popup__advanced').exists() it('should keep Advanced open when a commit reloads the same well', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('.mint-well-edit-popup__disclosure').trigger('click') await wrapper.setProps({ wellData: well('sample', { injectionVolume: 7 }) }) expect(advancedOpen(wrapper)).toBe(true) }) it('should reset Advanced when another well opens', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) await wrapper.get('.mint-well-edit-popup__disclosure').trigger('click') await wrapper.setProps({ wellId: 'C4', wellData: well('sample') }) expect(advancedOpen(wrapper)).toBe(false) }) it('should save a pending name before closing on Escape', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const order: string[] = [] await wrapper.setProps({ onSave: () => order.push('save'), onClose: () => order.push('close') }) await wrapper.get('[data-field="label"] input').setValue('S-0099') await wrapper.get('.mint-well-edit-popup').trigger('keydown', { key: 'Escape' }) expect(order).toEqual(['save', 'close']) }) it('should not save a pending name on Escape in the popover', async () => { const wrapper = mountWith({ wellData: well('sample') }) await wrapper.get('[data-field="label"] input').setValue('S-0099') await wrapper.get('.mint-well-edit-popup').trigger('keydown', { key: 'Escape' }) expect(wrapper.emitted('save')).toBeUndefined() }) }) describe('WellEditPopupInternal inline clear (7a)', () => { it('should keep focus in a pending field when Clear well is pressed, so the clear is one step', () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const press = new MouseEvent('mousedown', { bubbles: true, cancelable: true }) wrapper.get('[data-action="clear"]').element.dispatchEvent(press) expect(press.defaultPrevented).toBe(true) }) }) describe('WellEditPopupInternal inline clear discards pending edits (7a)', () => { it('should not save a pending name when focus leaves after Clear well', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const input = wrapper.get('[data-field="label"] input') await input.setValue('S-0099') await wrapper.get('[data-action="clear"]').trigger('click') await input.trigger('focusout') expect(wrapper.emitted('save')).toBeUndefined() }) }) describe('WellEditPopupInternal inline close (7a)', () => { it('should save a pending name before closing on Close', async () => { const wrapper = mountWith({ inline: true, wellData: well('sample') }) const order: string[] = [] await wrapper.setProps({ onSave: () => order.push('save'), onClose: () => order.push('close') }) await wrapper.get('[data-field="label"] input').setValue('S-0099') await wrapper.get('.mint-well-edit-popup__close').trigger('click') expect(order).toEqual(['save', 'close']) }) })