import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import SampleHierarchyTree from '../../components/SampleHierarchyTree.vue' import type { TreeNode } from '../../types' const nodes: TreeNode[] = [ { id: 'study', label: 'Study', type: 'study', children: [ { id: 'sample-1', label: 'Sample 1', type: 'sample' }, ], }, ] describe('SampleHierarchyTree', () => { it('uses shared expansion state for default expansion and node toggles', async () => { const wrapper = mount(SampleHierarchyTree, { props: { nodes, defaultExpandedIds: ['study'], }, }) expect(wrapper.find('.mint-sample-tree__children').exists()).toBe(true) await wrapper.find('.mint-sample-tree__toggle').trigger('click') expect(wrapper.emitted('collapse')?.[0]).toEqual(['study']) expect(wrapper.find('.mint-sample-tree__children').exists()).toBe(false) await wrapper.find('.mint-sample-tree__toggle').trigger('click') expect(wrapper.emitted('expand')?.[0]).toEqual(['study']) expect(wrapper.find('.mint-sample-tree__children').exists()).toBe(true) }) it('syncs expandAll with all tree node ids', async () => { const wrapper = mount(SampleHierarchyTree, { props: { nodes, expandAll: false, }, }) expect(wrapper.find('.mint-sample-tree__children').exists()).toBe(false) await wrapper.setProps({ expandAll: true }) expect(wrapper.find('.mint-sample-tree__children').exists()).toBe(true) }) it('should tag a sample label with its node type when the node is a sample', () => { const wrapper = mount(SampleHierarchyTree, { props: { nodes, defaultExpandedIds: ['study'] } }) const labels = wrapper.findAll('.mint-sample-tree__label') expect(labels[1].classes()).toContain('mint-sample-tree__label--sample') }) it('should wrap child nodes in the single grid row the expand animates', () => { const wrapper = mount(SampleHierarchyTree, { props: { nodes, defaultExpandedIds: ['study'] } }) expect(wrapper.find('.mint-sample-tree__children > .mint-sample-tree__children-inner > .mint-sample-tree__node').exists()).toBe(true) }) it('should own its child group from the parent treeitem when the node is expanded', () => { const wrapper = mount(SampleHierarchyTree, { props: { nodes, defaultExpandedIds: ['study'] } }) const owned = wrapper.get('[role="treeitem"]').attributes('aria-owns') expect(wrapper.find(`[id="${owned}"]`).attributes('role')).toBe('group') }) describe('selection (4c)', () => { const mountTree = (props: Record = {}) => mount(SampleHierarchyTree, { props: { nodes, defaultExpandedIds: ['study'], ...props } }) const headers = (wrapper: ReturnType) => wrapper.findAll('.mint-sample-tree__node-header') const items = (wrapper: ReturnType) => wrapper.findAll('[role="treeitem"]') it('should emit update:selectedId with the node id when a node is clicked', async () => { const wrapper = mountTree({ selectedId: null }) await headers(wrapper)[1].trigger('click') expect(wrapper.emitted('update:selectedId')?.[0]).toEqual(['sample-1']) }) it('should still emit node-click when a node is clicked', async () => { const wrapper = mountTree({ selectedId: null }) await headers(wrapper)[1].trigger('click') expect(wrapper.emitted('node-click')).toHaveLength(1) }) it('should mark the node header selected when selectedId matches the node', () => { const wrapper = mountTree({ selectedId: 'sample-1' }) expect(headers(wrapper)[1].classes()).toContain('mint-sample-tree__node-header--selected') }) it('should leave other node headers unselected when selectedId matches one node', () => { const wrapper = mountTree({ selectedId: 'sample-1' }) expect(headers(wrapper)[0].classes()).not.toContain('mint-sample-tree__node-header--selected') }) it('should set aria-selected on the matching treeitem when selectedId matches', () => { const wrapper = mountTree({ selectedId: 'sample-1' }) expect(items(wrapper)[1].attributes('aria-selected')).toBe('true') }) it('should set aria-selected false on other treeitems when a selection model is bound', () => { const wrapper = mountTree({ selectedId: 'sample-1' }) expect(items(wrapper)[0].attributes('aria-selected')).toBe('false') }) it('should omit aria-selected when no selectedId is bound', () => { const wrapper = mountTree() expect(items(wrapper)[0].attributes('aria-selected')).toBeUndefined() }) it('should make node headers focusable when a selection model is bound', () => { const wrapper = mountTree({ selectedId: null }) expect(headers(wrapper)[0].attributes('tabindex')).toBe('0') }) it('should keep node headers out of the tab order when no selectedId is bound', () => { const wrapper = mountTree() expect(headers(wrapper)[0].attributes('tabindex')).toBeUndefined() }) it('should select the node when Enter is pressed on its header', async () => { const wrapper = mountTree({ selectedId: null }) await headers(wrapper)[1].trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('update:selectedId')?.[0]).toEqual(['sample-1']) }) it('should select the node when Space is pressed on its header', async () => { const wrapper = mountTree({ selectedId: null }) await headers(wrapper)[1].trigger('keydown', { key: ' ' }) expect(wrapper.emitted('update:selectedId')?.[0]).toEqual(['sample-1']) }) it('should ignore Enter on a header when no selectedId is bound', async () => { const wrapper = mountTree() await headers(wrapper)[1].trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('update:selectedId')).toBeUndefined() }) it('should emit node-click when Enter is pressed on a header', async () => { const wrapper = mountTree({ selectedId: null }) await headers(wrapper)[1].trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('node-click')).toHaveLength(1) }) it('should put the treeitem role on the focusable header', () => { const wrapper = mountTree({ selectedId: null }) expect(headers(wrapper)[1].attributes('role')).toBe('treeitem') }) }) })