diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx index cfa7a681610c7..c520da155e473 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx @@ -1,10 +1,6 @@ import * as React from 'react'; -import { expect } from 'chai'; -import { spy } from 'sinon'; -import { act, createRenderer, ErrorBoundary, fireEvent, screen } from '@mui/internal-test-utils'; -import Portal from '@mui/material/Portal'; +import { createRenderer } from '@mui/internal-test-utils'; import { SimpleTreeView, simpleTreeViewClasses as classes } from '@mui/x-tree-view/SimpleTreeView'; -import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { describeConformance } from 'test/utils/describeConformance'; describe('', () => { @@ -18,168 +14,4 @@ describe('', () => { muiName: 'MuiSimpleTreeView', skip: ['componentProp', 'componentsProp', 'themeVariants'], })); - - describe('warnings', () => { - it('should not crash when shift clicking a clean tree', () => { - render( - - - - , - ); - - fireEvent.click(screen.getByText('one'), { shiftKey: true }); - }); - - it('should not crash when selecting multiple items in a deeply nested tree', () => { - render( - - - - - - - - , - ); - fireEvent.click(screen.getByText('Item 1.1.1')); - fireEvent.click(screen.getByText('Item 2'), { shiftKey: true }); - - expect(screen.getByTestId('item-1.1.1')).to.have.attribute('aria-selected', 'true'); - expect(screen.getByTestId('item-2')).to.have.attribute('aria-selected', 'true'); - }); - - it('should not crash when unmounting with duplicate ids', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function CustomTreeItem(props: any) { - return ; - } - function App() { - const [isVisible, hideTreeView] = React.useReducer(() => false, true); - - return ( - - - {isVisible && ( - - - - - - )} - - ); - } - const errorRef = React.createRef(); - render( - - - , - ); - - expect(() => { - act(() => { - screen.getByRole('button').click(); - }); - }).not.toErrorDev(); - }); - }); - - it('should call onKeyDown when a key is pressed', () => { - const handleTreeViewKeyDown = spy(); - const handleTreeItemKeyDown = spy(); - - const { getByTestId } = render( - - - , - ); - - const itemOne = getByTestId('one'); - act(() => { - itemOne.focus(); - }); - - fireEvent.keyDown(itemOne, { key: 'Enter' }); - fireEvent.keyDown(itemOne, { key: 'A' }); - fireEvent.keyDown(itemOne, { key: ']' }); - - expect(handleTreeViewKeyDown.callCount).to.equal(3); - expect(handleTreeItemKeyDown.callCount).to.equal(3); - }); - - it('should not error when component state changes', () => { - function MyComponent() { - const [, setState] = React.useState(1); - - return ( - { - setState(Math.random); - }} - > - - - - - ); - } - - const { getByTestId } = render(); - - fireEvent.focus(getByTestId('one')); - fireEvent.focus(getByTestId('one')); - expect(getByTestId('one')).toHaveFocus(); - - fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveFocus(); - - fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp' }); - - expect(getByTestId('one')).toHaveFocus(); - - fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveFocus(); - }); - - it('should work in a portal', () => { - const { getByTestId } = render( - - - - - - - - , - ); - - act(() => { - getByTestId('one').focus(); - }); - fireEvent.keyDown(getByTestId('one'), { key: 'ArrowDown' }); - - expect(getByTestId('two')).toHaveFocus(); - - fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown' }); - - expect(getByTestId('three')).toHaveFocus(); - - fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown' }); - - expect(getByTestId('four')).toHaveFocus(); - }); - - describe('Accessibility', () => { - it('(TreeView) should have the role `tree`', () => { - const { getByRole } = render(); - - expect(getByRole('tree')).not.to.equal(null); - }); - }); }); diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx index c1fca67e8ab29..6256ac0bb6a75 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; import { expect } from 'chai'; import PropTypes from 'prop-types'; -import { spy } from 'sinon'; -import { act, createEvent, createRenderer, fireEvent } from '@mui/internal-test-utils'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; +import { createRenderer } from '@mui/internal-test-utils'; import { SimpleTreeViewPlugins } from '@mui/x-tree-view/SimpleTreeView/SimpleTreeView.plugins'; import { TreeItem, treeItemClasses as classes } from '@mui/x-tree-view/TreeItem'; import { TreeViewContextValue } from '@mui/x-tree-view/internals/TreeViewProvider'; import { TreeViewContext } from '@mui/x-tree-view/internals/TreeViewProvider/TreeViewContext'; import { describeConformance } from 'test/utils/describeConformance'; +import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue = { instance: { @@ -45,6 +44,48 @@ const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue }, }; +describeTreeView<[]>('TreeItem component', ({ render, treeItemComponentName }) => { + describe('ContentComponent / ContentProps props (TreeItem only)', () => { + it('should use the ContentComponent prop when defined', function test() { + if (treeItemComponentName === 'TreeItem2') { + this.skip(); + } + + const ContentComponent = React.forwardRef((props: any, ref: React.Ref) => ( +
+ MOCK CONTENT COMPONENT +
+ )); + + const response = render({ + items: [{ id: '1' }], + slotProps: { item: { ContentComponent } }, + }); + + expect(response.getItemContent('1').textContent).to.equal('MOCK CONTENT COMPONENT'); + }); + + it('should use the ContentProps prop when defined', function test() { + if (treeItemComponentName === 'TreeItem2') { + this.skip(); + } + + const ContentComponent = React.forwardRef((props: any, ref: React.Ref) => ( +
+ {props.customProp} +
+ )); + + const response = render({ + items: [{ id: '1' }], + slotProps: { item: { ContentComponent, ContentProps: { customProp: 'ABCDEF' } as any } }, + }); + + expect(response.getItemContent('1').textContent).to.equal('ABCDEF'); + }); + }); +}); + describe('', () => { const { render } = createRenderer(); @@ -71,7 +112,7 @@ describe('', () => { skip: ['reactTestRenderer', 'componentProp', 'componentsProp', 'themeVariants'], })); - describe('warnings', () => { + describe('PropTypes warnings', () => { beforeEach(() => { PropTypes.resetWarningCache(); }); @@ -98,178 +139,4 @@ describe('', () => { }).toErrorDev('Expected an element type that can hold a ref.'); }); }); - - it('should call onClick when clicked', () => { - const handleClick = spy(); - - const { getByText } = render( - - - , - ); - - fireEvent.click(getByText('test')); - - expect(handleClick.callCount).to.equal(1); - }); - - it('should not call onClick when children are clicked', () => { - const handleClick = spy(); - - const { getByText } = render( - - - - - , - ); - - fireEvent.click(getByText('two')); - - expect(handleClick.callCount).to.equal(0); - }); - - describe('Accessibility', () => { - it('should have the role `treeitem`', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('test')).to.have.attribute('role', 'treeitem'); - }); - - it('should add the role `group` to a component containing children', () => { - const { getByRole, getByText } = render( - - - - - , - ); - - expect(getByRole('group')).to.contain(getByText('test2')); - }); - }); - - describe('prop: disabled', () => { - describe('event bindings', () => { - it('should not prevent onClick being fired', () => { - const handleClick = spy(); - - const { getByText } = render( - - - , - ); - - fireEvent.click(getByText('test')); - - expect(handleClick.callCount).to.equal(1); - }); - }); - }); - - describe('content customisation', () => { - it('should allow a custom ContentComponent', () => { - const mockContent = React.forwardRef((props: {}, ref: React.Ref) => ( -
MOCK CONTENT COMPONENT
- )); - const { container } = render( - - - , - ); - expect(container.textContent).to.equal('MOCK CONTENT COMPONENT'); - }); - - it('should allow props to be passed to a custom ContentComponent', () => { - const mockContent = React.forwardRef((props: any, ref: React.Ref) => ( -
{props.customProp}
- )); - const { container } = render( - - - , - ); - expect(container.textContent).to.equal('ABCDEF'); - }); - }); - - it('should be able to type in an child input', () => { - const { getByRole } = render( - - - - - - } - data-testid="two" - /> - - , - ); - const input = getByRole('textbox'); - const keydownEvent = createEvent.keyDown(input, { - key: 'a', - }); - - const handlePreventDefault = spy(); - keydownEvent.preventDefault = handlePreventDefault; - fireEvent(input, keydownEvent); - expect(handlePreventDefault.callCount).to.equal(0); - }); - - it('should not focus steal', () => { - let setActiveItemMounted; - // a TreeItem whose mounted state we can control with `setActiveItemMounted` - function ControlledTreeItem(props) { - const [mounted, setMounted] = React.useState(true); - setActiveItemMounted = setMounted; - - if (!mounted) { - return null; - } - return ; - } - const { getByText, getByTestId, getByRole } = render( - - - - - - - , - ); - - fireEvent.click(getByText('two')); - act(() => { - getByTestId('two').focus(); - }); - - expect(getByTestId('two')).toHaveFocus(); - - act(() => { - getByRole('button').focus(); - }); - - expect(getByRole('button')).toHaveFocus(); - - act(() => { - setActiveItemMounted(false); - }); - act(() => { - setActiveItemMounted(true); - }); - - expect(getByRole('button')).toHaveFocus(); - }); }); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx index 0db6df32a57c0..6bd9dc2448937 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { act, fireEvent } from '@mui/internal-test-utils'; @@ -14,176 +15,231 @@ import { */ describeTreeView< [UseTreeViewFocusSignature, UseTreeViewSelectionSignature, UseTreeViewItemsSignature] ->('useTreeViewFocus plugin', ({ render }) => { - describe('basic behavior', () => { - it('should allow to focus an item', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - }); +>( + 'useTreeViewFocus plugin', + ({ render, renderFromJSX, TreeItemComponent, treeViewComponentName, TreeViewComponent }) => { + describe('basic behavior', () => { + it('should allow to focus an item', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); - fireEvent.focus(response.getItemRoot('2')); - expect(response.getFocusedItemId()).to.equal('2'); + fireEvent.focus(response.getItemRoot('2')); + expect(response.getFocusedItemId()).to.equal('2'); - fireEvent.focus(response.getItemRoot('1')); - expect(response.getFocusedItemId()).to.equal('1'); - }); - - it('should move the focus when the focused item is removed', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], + fireEvent.focus(response.getItemRoot('1')); + expect(response.getFocusedItemId()).to.equal('1'); }); - fireEvent.focus(response.getItemRoot('2')); - expect(response.getFocusedItemId()).to.equal('2'); + it('should move the focus when the focused item is removed', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); - response.setItems([{ id: '1' }]); - expect(response.getFocusedItemId()).to.equal('1'); - }); - }); + fireEvent.focus(response.getItemRoot('2')); + expect(response.getFocusedItemId()).to.equal('2'); - describe('tabIndex HTML attribute', () => { - it('should set tabIndex={0} on the first item if none are selected', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], + response.setItems([{ id: '1' }]); + expect(response.getFocusedItemId()).to.equal('1'); }); - - expect(response.getItemRoot('1').tabIndex).to.equal(0); - expect(response.getItemRoot('2').tabIndex).to.equal(-1); }); - it('should set tabIndex={0} on the selected item (single selection)', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - selectedItems: '2', - }); - - expect(response.getItemRoot('1').tabIndex).to.equal(-1); - expect(response.getItemRoot('2').tabIndex).to.equal(0); - }); + describe('tabIndex HTML attribute', () => { + it('should set tabIndex={0} on the first item if none are selected', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); - it('should set tabIndex={0} on the first selected item (multi selection)', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - selectedItems: ['2', '3'], - multiSelect: true, + expect(response.getItemRoot('1').tabIndex).to.equal(0); + expect(response.getItemRoot('2').tabIndex).to.equal(-1); }); - expect(response.getItemRoot('1').tabIndex).to.equal(-1); - expect(response.getItemRoot('2').tabIndex).to.equal(0); - expect(response.getItemRoot('3').tabIndex).to.equal(-1); - }); + it('should set tabIndex={0} on the selected item (single selection)', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + selectedItems: '2', + }); - it('should set tabIndex={0} on the first item if the selected item is not visible', () => { - const response = render({ - items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }], - selectedItems: '2.1', + expect(response.getItemRoot('1').tabIndex).to.equal(-1); + expect(response.getItemRoot('2').tabIndex).to.equal(0); }); - expect(response.getItemRoot('1').tabIndex).to.equal(0); - expect(response.getItemRoot('2').tabIndex).to.equal(-1); - }); + it('should set tabIndex={0} on the first selected item (multi selection)', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + selectedItems: ['2', '3'], + multiSelect: true, + }); - it('should set tabIndex={0} on the first item if the no selected item is visible', () => { - const response = render({ - items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }, { id: '2.2' }] }], - selectedItems: ['2.1', '2.2'], - multiSelect: true, + expect(response.getItemRoot('1').tabIndex).to.equal(-1); + expect(response.getItemRoot('2').tabIndex).to.equal(0); + expect(response.getItemRoot('3').tabIndex).to.equal(-1); }); - expect(response.getItemRoot('1').tabIndex).to.equal(0); - expect(response.getItemRoot('2').tabIndex).to.equal(-1); - }); - }); - - describe('focusItem api method', () => { - it('should focus the item', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - }); + it('should set tabIndex={0} on the first item if the selected item is not visible', () => { + const response = render({ + items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }], + selectedItems: '2.1', + }); - act(() => { - response.apiRef.current.focusItem({} as any, '2'); + expect(response.getItemRoot('1').tabIndex).to.equal(0); + expect(response.getItemRoot('2').tabIndex).to.equal(-1); }); - expect(response.getFocusedItemId()).to.equal('2'); - }); - - it('should not focus item if parent is collapsed', () => { - const response = render({ - items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }], - }); + it('should set tabIndex={0} on the first item if the no selected item is visible', () => { + const response = render({ + items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }, { id: '2.2' }] }], + selectedItems: ['2.1', '2.2'], + multiSelect: true, + }); - act(() => { - response.apiRef.current.focusItem({} as any, '2.1'); + expect(response.getItemRoot('1').tabIndex).to.equal(0); + expect(response.getItemRoot('2').tabIndex).to.equal(-1); }); - - expect(response.getFocusedItemId()).to.equal(null); }); - }); - describe('onItemFocus prop', () => { - it('should be called when an item is focused', () => { - const onItemFocus = spy(); + describe('focusItem api method', () => { + it('should focus the item', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); - const response = render({ - items: [{ id: '1' }], - onItemFocus, - }); + act(() => { + response.apiRef.current.focusItem({} as any, '2'); + }); - act(() => { - response.getItemRoot('1').focus(); + expect(response.getFocusedItemId()).to.equal('2'); }); - expect(onItemFocus.callCount).to.equal(1); - expect(onItemFocus.lastCall.lastArg).to.equal('1'); - }); - }); - - describe('disabledItemsFocusable prop', () => { - describe('disabledItemFocusable={false}', () => { - it('should prevent focus by mouse', () => { + it('should not focus item if parent is collapsed', () => { const response = render({ - items: [{ id: '1', disabled: true }], - disabledItemsFocusable: false, + items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }], + }); + + act(() => { + response.apiRef.current.focusItem({} as any, '2.1'); }); - fireEvent.click(response.getItemContent('1')); expect(response.getFocusedItemId()).to.equal(null); }); + }); + + describe('onItemFocus prop', () => { + it('should be called when an item is focused', () => { + const onItemFocus = spy(); - it('should tab tabIndex={-1} on the disabled item and tabIndex={0} on the first non-disabled item', () => { const response = render({ - items: [{ id: '1', disabled: true }, { id: '2' }, { id: '3' }], - disabledItemsFocusable: false, + items: [{ id: '1' }], + onItemFocus, }); - expect(response.getItemRoot('1').tabIndex).to.equal(-1); - expect(response.getItemRoot('2').tabIndex).to.equal(0); - expect(response.getItemRoot('3').tabIndex).to.equal(-1); + act(() => { + response.getItemRoot('1').focus(); + }); + + expect(onItemFocus.callCount).to.equal(1); + expect(onItemFocus.lastCall.lastArg).to.equal('1'); }); }); - describe('disabledItemFocusable={true}', () => { - it('should prevent focus by mouse', () => { - const response = render({ - items: [{ id: '1', disabled: true }], - disabledItemsFocusable: true, + describe('disabledItemsFocusable prop', () => { + describe('disabledItemFocusable={false}', () => { + it('should prevent focus by mouse', () => { + const response = render({ + items: [{ id: '1', disabled: true }], + disabledItemsFocusable: false, + }); + + fireEvent.click(response.getItemContent('1')); + expect(response.getFocusedItemId()).to.equal(null); }); - fireEvent.click(response.getItemContent('1')); - expect(response.getFocusedItemId()).to.equal(null); + it('should tab tabIndex={-1} on the disabled item and tabIndex={0} on the first non-disabled item', () => { + const response = render({ + items: [{ id: '1', disabled: true }, { id: '2' }, { id: '3' }], + disabledItemsFocusable: false, + }); + + expect(response.getItemRoot('1').tabIndex).to.equal(-1); + expect(response.getItemRoot('2').tabIndex).to.equal(0); + expect(response.getItemRoot('3').tabIndex).to.equal(-1); + }); }); - it('should tab tabIndex={0} on the disabled item and tabIndex={-1} on the other items', () => { - const response = render({ - items: [{ id: '1', disabled: true }, { id: '2' }, { id: '3' }], - disabledItemsFocusable: true, + describe('disabledItemFocusable={true}', () => { + it('should prevent focus by mouse', () => { + const response = render({ + items: [{ id: '1', disabled: true }], + disabledItemsFocusable: true, + }); + + fireEvent.click(response.getItemContent('1')); + expect(response.getFocusedItemId()).to.equal(null); }); - expect(response.getItemRoot('1').tabIndex).to.equal(0); - expect(response.getItemRoot('2').tabIndex).to.equal(-1); - expect(response.getItemRoot('3').tabIndex).to.equal(-1); + it('should tab tabIndex={0} on the disabled item and tabIndex={-1} on the other items', () => { + const response = render({ + items: [{ id: '1', disabled: true }, { id: '2' }, { id: '3' }], + disabledItemsFocusable: true, + }); + + expect(response.getItemRoot('1').tabIndex).to.equal(0); + expect(response.getItemRoot('2').tabIndex).to.equal(-1); + expect(response.getItemRoot('3').tabIndex).to.equal(-1); + }); }); }); - }); -}); + + it('should not error when component state changes', () => { + const items = [{ id: '1', children: [{ id: '1.1' }] }]; + const getItemLabel = (item) => item.id; + + function MyComponent() { + const [, setState] = React.useState(1); + + if (treeViewComponentName === 'SimpleTreeView') { + return ( + { + setState(Math.random); + }} + > + + + + + ); + } + + return ( + { + setState(Math.random); + }} + slotProps={{ + item: (ownerState) => ({ 'data-testid': ownerState.itemId }) as any, + }} + getItemLabel={getItemLabel} + /> + ); + } + + const response = renderFromJSX(); + + fireEvent.focus(response.getItemRoot('1')); + expect(response.getFocusedItemId()).to.equal('1'); + + fireEvent.keyDown(response.getItemRoot('1'), { key: 'ArrowDown' }); + expect(response.getFocusedItemId()).to.equal('1.1'); + + fireEvent.keyDown(response.getItemRoot('1.1'), { key: 'ArrowUp' }); + expect(response.getFocusedItemId()).to.equal('1'); + + fireEvent.keyDown(response.getItemRoot('1'), { key: 'ArrowDown' }); + expect(response.getFocusedItemId()).to.equal('1.1'); + }); + }, +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx index 96493e3e15666..790fc886213bd 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { expect } from 'chai'; +import { spy } from 'sinon'; import { act, fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; import { @@ -1250,4 +1251,29 @@ describeTreeView< expect(response.getFocusedItemId()).to.equal('1'); }); }); + + describe('onKeyDown prop', () => { + it('should call onKeyDown on the Tree View and the Tree Item when a key is pressed', () => { + const handleTreeViewKeyDown = spy(); + const handleTreeItemKeyDown = spy(); + + const response = render({ + items: [{ id: '1' }], + onKeyDown: handleTreeViewKeyDown, + slotProps: { item: { onKeyDown: handleTreeItemKeyDown } }, + } as any); + + const itemRoot = response.getItemRoot('1'); + act(() => { + itemRoot.focus(); + }); + + fireEvent.keyDown(itemRoot, { key: 'Enter' }); + fireEvent.keyDown(itemRoot, { key: 'A' }); + fireEvent.keyDown(itemRoot, { key: ']' }); + + expect(handleTreeViewKeyDown.callCount).to.equal(3); + expect(handleTreeItemKeyDown.callCount).to.equal(3); + }); + }); }); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx index 6af3f4b2ca00e..6bd4595d4c060 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx @@ -2,765 +2,797 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import { fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; -import { UseTreeViewSelectionSignature } from '@mui/x-tree-view/internals'; +import { + UseTreeViewExpansionSignature, + UseTreeViewSelectionSignature, +} from '@mui/x-tree-view/internals'; /** * All tests related to keyboard navigation (e.g.: selection using "Space") * are located in the `useTreeViewKeyboardNavigation.test.tsx` file. */ -describeTreeView<[UseTreeViewSelectionSignature]>('useTreeViewSelection plugin', ({ render }) => { - describe('model props (selectedItems, defaultSelectedItems, onSelectedItemsChange)', () => { - it('should not select items when no default state and no control state are defined', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - }); - - expect(response.isItemSelected('1')).to.equal(false); - }); +describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]>( + 'useTreeViewSelection plugin', + ({ render }) => { + describe('model props (selectedItems, defaultSelectedItems, onSelectedItemsChange)', () => { + it('should not select items when no default state and no control state are defined', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); - it('should use the default state when defined', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(true); - }); + it('should use the default state when defined', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1'], + }); - it('should use the controlled state when defined', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - selectedItems: ['1'], + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.isItemSelected('1')).to.equal(true); - }); + it('should use the controlled state when defined', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + selectedItems: ['1'], + }); - it('should use the controlled state instead of the default state when both are defined', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - selectedItems: ['1'], - defaultSelectedItems: ['2'], + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.isItemSelected('1')).to.equal(true); - }); + it('should use the controlled state instead of the default state when both are defined', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + selectedItems: ['1'], + defaultSelectedItems: ['2'], + }); - it('should react to controlled state update', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - selectedItems: [], + expect(response.isItemSelected('1')).to.equal(true); }); - response.setProps({ selectedItems: ['1'] }); - expect(response.isItemSelected('1')).to.equal(true); - }); - - it('should call the onSelectedItemsChange callback when the model is updated (single selection and add selected item)', () => { - const onSelectedItemsChange = spy(); + it('should react to controlled state update', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + selectedItems: [], + }); - const response = render({ - items: [{ id: '1' }, { id: '2' }], - onSelectedItemsChange, + response.setProps({ selectedItems: ['1'] }); + expect(response.isItemSelected('1')).to.equal(true); }); - fireEvent.click(response.getItemContent('1')); + it('should call the onSelectedItemsChange callback when the model is updated (single selection and add selected item)', () => { + const onSelectedItemsChange = spy(); - expect(onSelectedItemsChange.callCount).to.equal(1); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal('1'); - }); + const response = render({ + items: [{ id: '1' }, { id: '2' }], + onSelectedItemsChange, + }); - // TODO: Re-enable this test if we have a way to un-select an item in single selection. - // eslint-disable-next-line mocha/no-skipped-tests - it.skip('should call onSelectedItemsChange callback when the model is updated (single selection and remove selected item', () => { - const onSelectedItemsChange = spy(); + fireEvent.click(response.getItemContent('1')); - const response = render({ - items: [{ id: '1' }, { id: '2' }], - onSelectedItemsChange, - defaultSelectedItems: ['1'], + expect(onSelectedItemsChange.callCount).to.equal(1); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal('1'); }); - fireEvent.click(response.getItemContent('1')); + // TODO: Re-enable this test if we have a way to un-select an item in single selection. + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('should call onSelectedItemsChange callback when the model is updated (single selection and remove selected item', () => { + const onSelectedItemsChange = spy(); - expect(onSelectedItemsChange.callCount).to.equal(1); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal([]); - }); + const response = render({ + items: [{ id: '1' }, { id: '2' }], + onSelectedItemsChange, + defaultSelectedItems: ['1'], + }); - it('should call the onSelectedItemsChange callback when the model is updated (multi selection and add selected item to empty list)', () => { - const onSelectedItemsChange = spy(); + fireEvent.click(response.getItemContent('1')); - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - onSelectedItemsChange, + expect(onSelectedItemsChange.callCount).to.equal(1); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal([]); }); - fireEvent.click(response.getItemContent('1')); + it('should call the onSelectedItemsChange callback when the model is updated (multi selection and add selected item to empty list)', () => { + const onSelectedItemsChange = spy(); - expect(onSelectedItemsChange.callCount).to.equal(1); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1']); - }); + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + onSelectedItemsChange, + }); - it('should call the onSelectedItemsChange callback when the model is updated (multi selection and add selected item to non-empty list)', () => { - const onSelectedItemsChange = spy(); + fireEvent.click(response.getItemContent('1')); - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - onSelectedItemsChange, - defaultSelectedItems: ['1'], + expect(onSelectedItemsChange.callCount).to.equal(1); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1']); }); - fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); + it('should call the onSelectedItemsChange callback when the model is updated (multi selection and add selected item to non-empty list)', () => { + const onSelectedItemsChange = spy(); - expect(onSelectedItemsChange.callCount).to.equal(1); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['2', '1']); - }); + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + onSelectedItemsChange, + defaultSelectedItems: ['1'], + }); - it('should call the onSelectedItemsChange callback when the model is updated (multi selection and remove selected item)', () => { - const onSelectedItemsChange = spy(); + fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - onSelectedItemsChange, - defaultSelectedItems: ['1'], + expect(onSelectedItemsChange.callCount).to.equal(1); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['2', '1']); }); - fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); + it('should call the onSelectedItemsChange callback when the model is updated (multi selection and remove selected item)', () => { + const onSelectedItemsChange = spy(); - expect(onSelectedItemsChange.callCount).to.equal(1); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal([]); - }); - - it('should warn when switching from controlled to uncontrolled', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - selectedItems: [], - }); + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + onSelectedItemsChange, + defaultSelectedItems: ['1'], + }); - expect(() => { - response.setProps({ selectedItems: undefined }); - }).toErrorDev( - 'MUI X: A component is changing the controlled selectedItems state of TreeView to be uncontrolled.', - ); - }); + fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); - it('should warn and not react to update when updating the default state', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], + expect(onSelectedItemsChange.callCount).to.equal(1); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal([]); }); - expect(() => { - response.setProps({ defaultSelectedItems: ['2'] }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - }).toErrorDev( - 'MUI X: A component is changing the default selectedItems state of an uncontrolled TreeView after being initialized. To suppress this warning opt to use a controlled TreeView.', - ); - }); - }); - - describe('item click interaction', () => { - describe('single selection', () => { - it('should select un-selected item when clicking on an item content', () => { + it('should warn when switching from controlled to uncontrolled', () => { const response = render({ items: [{ id: '1' }, { id: '2' }], + selectedItems: [], }); - expect(response.isItemSelected('1')).to.equal(false); - - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(true); + expect(() => { + response.setProps({ selectedItems: undefined }); + }).toErrorDev( + 'MUI X: A component is changing the controlled selectedItems state of TreeView to be uncontrolled.', + ); }); - it('should not un-select selected item when clicking on an item content', () => { + it('should warn and not react to update when updating the default state', () => { const response = render({ items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: '1', + defaultSelectedItems: ['1'], }); - expect(response.isItemSelected('1')).to.equal(true); - - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(true); + expect(() => { + response.setProps({ defaultSelectedItems: ['2'] }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + }).toErrorDev( + 'MUI X: A component is changing the default selectedItems state of an uncontrolled TreeView after being initialized. To suppress this warning opt to use a controlled TreeView.', + ); }); + }); - it('should not select an item when click and disableSelection', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - disableSelection: true, + describe('item click interaction', () => { + describe('single selection', () => { + it('should select un-selected item when clicking on an item content', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); + + expect(response.isItemSelected('1')).to.equal(false); + + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should not un-select selected item when clicking on an item content', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: '1', + }); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(true); - it('should not select an item when clicking on a disabled item content', () => { - const response = render({ - items: [{ id: '1', disabled: true }, { id: '2' }], + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.isItemSelected('1')).to.equal(false); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); - }); + it('should not select an item when click and disableSelection', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + disableSelection: true, + }); - describe('multi selection', () => { - it('should select un-selected item and remove other selected items when clicking on an item content', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['2'], + expect(response.isItemSelected('1')).to.equal(false); + + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should not select an item when clicking on a disabled item content', () => { + const response = render({ + items: [{ id: '1', disabled: true }, { id: '2' }], + }); - fireEvent.click(response.getItemContent('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + expect(response.isItemSelected('1')).to.equal(false); + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); + }); }); - it('should not un-select selected item when clicking on an item content', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], + describe('multi selection', () => { + it('should select un-selected item and remove other selected items when clicking on an item content', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['2'], + }); + + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + + fireEvent.click(response.getItemContent('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); }); - expect(response.isItemSelected('1')).to.equal(true); + it('should not un-select selected item when clicking on an item content', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1'], + }); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(true); - }); + expect(response.isItemSelected('1')).to.equal(true); - it('should un-select selected item when clicking on its content while holding Ctrl', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1', '2'], + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); - fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - }); + it('should un-select selected item when clicking on its content while holding Ctrl', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1', '2'], + }); - it('should un-select selected item when clicking on its content while holding Meta', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1', '2'], + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); + fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); + it('should un-select selected item when clicking on its content while holding Meta', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1', '2'], + }); - fireEvent.click(response.getItemContent('1'), { metaKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); - it('should not select an item when click and disableSelection', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - disableSelection: true, + fireEvent.click(response.getItemContent('1'), { metaKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should not select an item when click and disableSelection', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + disableSelection: true, + }); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(false); - it('should not select an item when clicking on a disabled item content', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1', disabled: true }, { id: '2' }], + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + it('should not select an item when clicking on a disabled item content', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1', disabled: true }, { id: '2' }], + }); - it('should select un-selected item when clicking on its content while holding Ctrl', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - defaultSelectedItems: ['1'], + expect(response.isItemSelected('1')).to.equal(false); + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + it('should select un-selected item when clicking on its content while holding Ctrl', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + defaultSelectedItems: ['1'], + }); - fireEvent.click(response.getItemContent('3'), { ctrlKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); - }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - it('should expand the selection range when clicking on an item content below the last selected item while holding Shift', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + fireEvent.click(response.getItemContent('3'), { ctrlKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); }); - fireEvent.click(response.getItemContent('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should do nothing when clicking on an item content on a fresh tree whil holding Shift', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemContent('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); - }); + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal([]); + }); - it('should expand the selection range when clicking on an item content above the last selected item while holding Shift', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + it('should expand the selection range when clicking on an item content below the last selected item while holding Shift', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); + + fireEvent.click(response.getItemContent('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); }); - fireEvent.click(response.getItemContent('3')); - expect(response.getSelectedTreeItems()).to.deep.equal(['3']); + it('should expand the selection range when clicking on an item content above the last selected item while holding Shift', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemContent('2'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); - }); + fireEvent.click(response.getItemContent('3')); + expect(response.getSelectedTreeItems()).to.deep.equal(['3']); - it('should expand the selection range when clicking on an item content while holding Shift after un-selecting another item', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + fireEvent.click(response.getItemContent('2'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); }); - fireEvent.click(response.getItemContent('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + it('should expand the selection range when clicking on an item content while holding Shift after un-selecting another item', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); + fireEvent.click(response.getItemContent('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); - fireEvent.click(response.getItemContent('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2', '2.1', '3']); - }); + fireEvent.click(response.getItemContent('2'), { ctrlKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - it('should not expand the selection range when clicking on a disabled item content then clicking on an item content while holding Shift', () => { - const response = render({ - multiSelect: true, - items: [ - { id: '1' }, - { id: '2', disabled: true }, - { id: '2.1' }, - { id: '3' }, - { id: '4' }, - ], + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2', '2.1', '3']); }); - fireEvent.click(response.getItemContent('2')); - expect(response.getSelectedTreeItems()).to.deep.equal([]); + it('should not expand the selection range when clicking on a disabled item content then clicking on an item content while holding Shift', () => { + const response = render({ + multiSelect: true, + items: [ + { id: '1' }, + { id: '2', disabled: true }, + { id: '2.1' }, + { id: '3' }, + { id: '4' }, + ], + }); - fireEvent.click(response.getItemContent('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal([]); - }); + fireEvent.click(response.getItemContent('2')); + expect(response.getSelectedTreeItems()).to.deep.equal([]); - it('should not expand the selection range when clicking on an item content then clicking a disabled item content while holding Shift', () => { - const response = render({ - multiSelect: true, - items: [ - { id: '1' }, - { id: '2' }, - { id: '2.1' }, - { id: '3', disabled: true }, - { id: '4' }, - ], + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal([]); }); - fireEvent.click(response.getItemContent('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should not expand the selection range when clicking on an item content then clicking a disabled item content while holding Shift', () => { + const response = render({ + multiSelect: true, + items: [ + { id: '1' }, + { id: '2' }, + { id: '2.1' }, + { id: '3', disabled: true }, + { id: '4' }, + ], + }); - fireEvent.click(response.getItemContent('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - }); + fireEvent.click(response.getItemContent('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - it('should not select disabled items that are part of the selected range', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2', disabled: true }, { id: '3' }], + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); }); - fireEvent.click(response.getItemContent('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + it('should not select disabled items that are part of the selected range', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2', disabled: true }, { id: '3' }], + }); - fireEvent.click(response.getItemContent('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); - }); - }); - }); + fireEvent.click(response.getItemContent('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - describe('checkbox interaction', () => { - describe('render checkbox when needed', () => { - it('should not render a checkbox when checkboxSelection is not defined', () => { - const response = render({ - items: [{ id: '1' }], + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); }); - expect(response.getItemCheckbox('1')).to.equal(null); - }); + it('should not crash when selecting multiple items in a deeply nested tree', () => { + const response = render({ + multiSelect: true, + items: [ + { id: '1', children: [{ id: '1.1', children: [{ id: '1.1.1' }] }] }, + { id: '2' }, + ], + defaultExpandedItems: ['1', '1.1'], + }); - it('should not render a checkbox when checkboxSelection is false', () => { - const response = render({ - checkboxSelection: false, - items: [{ id: '1' }], - }); + fireEvent.click(response.getItemContent('1.1.1')); + fireEvent.click(response.getItemContent('2'), { shiftKey: true }); - expect(response.getItemCheckbox('1')).to.equal(null); + expect(response.getSelectedTreeItems()).to.deep.equal(['1.1.1', '2']); + }); }); + }); - it('should render a checkbox when checkboxSelection is true', () => { - const response = render({ - checkboxSelection: true, - items: [{ id: '1' }], + describe('checkbox interaction', () => { + describe('render checkbox when needed', () => { + it('should not render a checkbox when checkboxSelection is not defined', () => { + const response = render({ + items: [{ id: '1' }], + }); + + expect(response.getItemCheckbox('1')).to.equal(null); }); - expect(response.getItemCheckbox('1')).not.to.equal(null); - }); - }); + it('should not render a checkbox when checkboxSelection is false', () => { + const response = render({ + checkboxSelection: false, + items: [{ id: '1' }], + }); - describe('single selection', () => { - it('should not change selection when clicking on an item content', () => { - const response = render({ - checkboxSelection: true, - items: [{ id: '1' }], + expect(response.getItemCheckbox('1')).to.equal(null); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should render a checkbox when checkboxSelection is true', () => { + const response = render({ + checkboxSelection: true, + items: [{ id: '1' }], + }); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); + expect(response.getItemCheckbox('1')).not.to.equal(null); + }); }); - it('should select un-selected item when clicking on an item checkbox', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - checkboxSelection: true, + describe('single selection', () => { + it('should not change selection when clicking on an item content', () => { + const response = render({ + checkboxSelection: true, + items: [{ id: '1' }], + }); + + expect(response.isItemSelected('1')).to.equal(false); + + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should select un-selected item when clicking on an item checkbox', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + checkboxSelection: true, + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(true); - }); + expect(response.isItemSelected('1')).to.equal(false); - it('should un-select selected item when clicking on an item checkbox', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: '1', - checkboxSelection: true, + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(true); }); - expect(response.isItemSelected('1')).to.equal(true); + it('should un-select selected item when clicking on an item checkbox', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: '1', + checkboxSelection: true, + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(true); - it('should not select an item when click and disableSelection', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - disableSelection: true, - checkboxSelection: true, + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should not select an item when click and disableSelection', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + disableSelection: true, + checkboxSelection: true, + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(false); - it('should not select an item when clicking on a disabled item checkbox', () => { - const response = render({ - items: [{ id: '1', disabled: true }, { id: '2' }], - checkboxSelection: true, + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); - }); + it('should not select an item when clicking on a disabled item checkbox', () => { + const response = render({ + items: [{ id: '1', disabled: true }, { id: '2' }], + checkboxSelection: true, + }); - describe('multi selection', () => { - it('should not change selection when clicking on an item content', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }], + expect(response.isItemSelected('1')).to.equal(false); + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); + }); - expect(response.isItemSelected('1')).to.equal(false); + describe('multi selection', () => { + it('should not change selection when clicking on an item content', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }], + }); - fireEvent.click(response.getItemContent('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(false); - it('should select un-selected item and keep other items selected when clicking on an item checkbox', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['2'], + fireEvent.click(response.getItemContent('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should select un-selected item and keep other items selected when clicking on an item checkbox', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['2'], + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); - }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - it('should un-select selected item when clicking on an item checkbox', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); }); - expect(response.isItemSelected('1')).to.equal(true); + it('should un-select selected item when clicking on an item checkbox', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1'], + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(true); - it('should not select an item when click and disableSelection', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }], - disableSelection: true, + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); + it('should not select an item when click and disableSelection', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }], + disableSelection: true, + }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + expect(response.isItemSelected('1')).to.equal(false); - it('should not select an item when clicking on a disabled item content', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1', disabled: true }, { id: '2' }], + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - expect(response.isItemSelected('1')).to.equal(false); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.isItemSelected('1')).to.equal(false); - }); + it('should not select an item when clicking on a disabled item content', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1', disabled: true }, { id: '2' }], + }); - it('should expand the selection range when clicking on an item checkbox below the last selected item while holding Shift', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + expect(response.isItemSelected('1')).to.equal(false); + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.isItemSelected('1')).to.equal(false); }); - fireEvent.click(response.getItemCheckboxInput('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should expand the selection range when clicking on an item checkbox below the last selected item while holding Shift', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); - }); + fireEvent.click(response.getItemCheckboxInput('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - it('should expand the selection range when clicking on an item checkbox above the last selected item while holding Shift', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); }); - fireEvent.click(response.getItemCheckboxInput('3')); - expect(response.getSelectedTreeItems()).to.deep.equal(['3']); + it('should expand the selection range when clicking on an item checkbox above the last selected item while holding Shift', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemCheckboxInput('2'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); - }); + fireEvent.click(response.getItemCheckboxInput('3')); + expect(response.getSelectedTreeItems()).to.deep.equal(['3']); - it('should expand the selection range when clicking on an item checkbox while holding Shift after un-selecting another item', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + fireEvent.click(response.getItemCheckboxInput('2'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2', '2.1', '3']); }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + it('should expand the selection range when clicking on an item checkbox while holding Shift after un-selecting another item', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }], + }); - fireEvent.click(response.getItemCheckboxInput('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - fireEvent.click(response.getItemCheckboxInput('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + fireEvent.click(response.getItemCheckboxInput('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2']); - fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2', '2.1', '3']); - }); + fireEvent.click(response.getItemCheckboxInput('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - it('should not expand the selection range when clicking on a disabled item checkbox then clicking on an item checkbox while holding Shift', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [ - { id: '1' }, - { id: '2', disabled: true }, - { id: '2.1' }, - { id: '3' }, - { id: '4' }, - ], + fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '2', '2.1', '3']); }); - fireEvent.click(response.getItemCheckboxInput('2')); - expect(response.getSelectedTreeItems()).to.deep.equal([]); + it('should not expand the selection range when clicking on a disabled item checkbox then clicking on an item checkbox while holding Shift', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [ + { id: '1' }, + { id: '2', disabled: true }, + { id: '2.1' }, + { id: '3' }, + { id: '4' }, + ], + }); - fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal([]); - }); + fireEvent.click(response.getItemCheckboxInput('2')); + expect(response.getSelectedTreeItems()).to.deep.equal([]); - it('should not expand the selection range when clicking on an item checkbox then clicking a disabled item checkbox while holding Shift', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [ - { id: '1' }, - { id: '2' }, - { id: '2.1' }, - { id: '3', disabled: true }, - { id: '4' }, - ], + fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal([]); }); - fireEvent.click(response.getItemCheckboxInput('2')); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); + it('should not expand the selection range when clicking on an item checkbox then clicking a disabled item checkbox while holding Shift', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [ + { id: '1' }, + { id: '2' }, + { id: '2.1' }, + { id: '3', disabled: true }, + { id: '4' }, + ], + }); - fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - }); + fireEvent.click(response.getItemCheckboxInput('2')); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); - it('should not select disabled items that are part of the selected range', () => { - const response = render({ - multiSelect: true, - checkboxSelection: true, - items: [{ id: '1' }, { id: '2', disabled: true }, { id: '3' }], + fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['2']); }); - fireEvent.click(response.getItemCheckboxInput('1')); - expect(response.getSelectedTreeItems()).to.deep.equal(['1']); + it('should not select disabled items that are part of the selected range', () => { + const response = render({ + multiSelect: true, + checkboxSelection: true, + items: [{ id: '1' }, { id: '2', disabled: true }, { id: '3' }], + }); - fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); - expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); - }); - }); - }); + fireEvent.click(response.getItemCheckboxInput('1')); + expect(response.getSelectedTreeItems()).to.deep.equal(['1']); - describe('aria-multiselectable tree attribute', () => { - it('should have the attribute `aria-multiselectable=false if using single select`', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], + fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true }); + expect(response.getSelectedTreeItems()).to.deep.equal(['1', '3']); + }); }); - - expect(response.getRoot()).to.have.attribute('aria-multiselectable', 'false'); }); - it('should have the attribute `aria-multiselectable=true if using multi select`', () => { - const response = render({ items: [{ id: '1' }, { id: '2' }], multiSelect: true }); - - expect(response.getRoot()).to.have.attribute('aria-multiselectable', 'true'); - }); - }); - - // The `aria-selected` attribute is used by the `response.isItemSelected` method. - // This `describe` only tests basics scenarios, more complex scenarios are tested in this file's other `describe`. - describe('aria-selected item attribute', () => { - describe('single selection', () => { - it('should not have the attribute `aria-selected=false` if not selected', () => { + describe('aria-multiselectable tree attribute', () => { + it('should have the attribute `aria-multiselectable=false if using single select`', () => { const response = render({ items: [{ id: '1' }, { id: '2' }], }); - expect(response.getItemRoot('1')).not.to.have.attribute('aria-selected'); + expect(response.getRoot()).to.have.attribute('aria-multiselectable', 'false'); }); - it('should have the attribute `aria-selected=true` if selected', () => { - const response = render({ - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: '1', - }); + it('should have the attribute `aria-multiselectable=true if using multi select`', () => { + const response = render({ items: [{ id: '1' }, { id: '2' }], multiSelect: true }); - expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'true'); + expect(response.getRoot()).to.have.attribute('aria-multiselectable', 'true'); }); }); - describe('multi selection', () => { - it('should have the attribute `aria-selected=false` if not selected', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], + // The `aria-selected` attribute is used by the `response.isItemSelected` method. + // This `describe` only tests basics scenarios, more complex scenarios are tested in this file's other `describe`. + describe('aria-selected item attribute', () => { + describe('single selection', () => { + it('should not have the attribute `aria-selected=false` if not selected', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); + + expect(response.getItemRoot('1')).not.to.have.attribute('aria-selected'); }); - expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); + it('should have the attribute `aria-selected=true` if selected', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: '1', + }); + + expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'true'); + }); }); - it('should have the attribute `aria-selected=true` if selected', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], + describe('multi selection', () => { + it('should have the attribute `aria-selected=false` if not selected', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + }); + + expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); }); - expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'true'); - }); + it('should have the attribute `aria-selected=true` if selected', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1'], + }); - it('should have the attribute `aria-selected=false` if disabledSelection is true', () => { - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - disableSelection: true, + expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'true'); }); - expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); + it('should have the attribute `aria-selected=false` if disabledSelection is true', () => { + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + disableSelection: true, + }); + + expect(response.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); + }); }); }); - }); - describe('onItemSelectionToggle prop', () => { - it('should call the onItemSelectionToggle callback when selecting an item', () => { - const onItemSelectionToggle = spy(); + describe('onItemSelectionToggle prop', () => { + it('should call the onItemSelectionToggle callback when selecting an item', () => { + const onItemSelectionToggle = spy(); - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - onItemSelectionToggle, + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + onItemSelectionToggle, + }); + + fireEvent.click(response.getItemContent('1')); + expect(onItemSelectionToggle.callCount).to.equal(1); + expect(onItemSelectionToggle.lastCall.args[1]).to.equal('1'); + expect(onItemSelectionToggle.lastCall.args[2]).to.equal(true); }); - fireEvent.click(response.getItemContent('1')); - expect(onItemSelectionToggle.callCount).to.equal(1); - expect(onItemSelectionToggle.lastCall.args[1]).to.equal('1'); - expect(onItemSelectionToggle.lastCall.args[2]).to.equal(true); - }); + it('should call the onItemSelectionToggle callback when un-selecting an item', () => { + const onItemSelectionToggle = spy(); - it('should call the onItemSelectionToggle callback when un-selecting an item', () => { - const onItemSelectionToggle = spy(); + const response = render({ + multiSelect: true, + items: [{ id: '1' }, { id: '2' }], + defaultSelectedItems: ['1'], + onItemSelectionToggle, + }); - const response = render({ - multiSelect: true, - items: [{ id: '1' }, { id: '2' }], - defaultSelectedItems: ['1'], - onItemSelectionToggle, + fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); + expect(onItemSelectionToggle.callCount).to.equal(1); + expect(onItemSelectionToggle.lastCall.args[1]).to.equal('1'); + expect(onItemSelectionToggle.lastCall.args[2]).to.equal(false); }); - - fireEvent.click(response.getItemContent('1'), { ctrlKey: true }); - expect(onItemSelectionToggle.callCount).to.equal(1); - expect(onItemSelectionToggle.lastCall.args[1]).to.equal('1'); - expect(onItemSelectionToggle.lastCall.args[2]).to.equal(false); }); - }); -}); + }, +); diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx b/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx new file mode 100644 index 0000000000000..2fba6b0a8ab97 --- /dev/null +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { fireEvent, act } from '@mui/internal-test-utils'; +import { + describeTreeView, + DescribeTreeViewRendererUtils, +} from 'test/utils/tree-view/describeTreeView'; + +describeTreeView<[]>( + 'useTreeView hook', + ({ render, renderFromJSX, treeViewComponentName, TreeViewComponent, TreeItemComponent }) => { + it('should have the role="tree" on the root slot', () => { + const response = render({ items: [{ id: '1' }] }); + + expect(response.getRoot()).to.have.attribute('role', 'tree'); + }); + + it('should work inside a Portal', () => { + let response: DescribeTreeViewRendererUtils; + if (treeViewComponentName === 'SimpleTreeView') { + response = renderFromJSX( + + + + + + + + + , + ); + } else { + response = renderFromJSX( + + + ({ 'data-testid': ownerState.itemId }) as any, + }} + getItemLabel={(item) => item.id} + /> + , + ); + } + + act(() => { + response.getItemRoot('1').focus(); + }); + + fireEvent.keyDown(response.getItemRoot('1'), { key: 'ArrowDown' }); + expect(response.getFocusedItemId()).to.equal('2'); + + fireEvent.keyDown(response.getItemRoot('2'), { key: 'ArrowDown' }); + expect(response.getFocusedItemId()).to.equal('3'); + + fireEvent.keyDown(response.getItemRoot('3'), { key: 'ArrowDown' }); + expect(response.getFocusedItemId()).to.equal('4'); + }); + }, +); diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.test.tsx b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.test.tsx new file mode 100644 index 0000000000000..946815953ccb7 --- /dev/null +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.test.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { act, createEvent, fireEvent, screen } from '@mui/monorepo/packages/test-utils'; +import { + describeTreeView, + DescribeTreeViewRendererUtils, +} from 'test/utils/tree-view/describeTreeView'; +import { + UseTreeViewExpansionSignature, + UseTreeViewIconsSignature, +} from '@mui/x-tree-view/internals'; +import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; + +describeTreeView<[UseTreeViewExpansionSignature, UseTreeViewIconsSignature]>( + 'useTreeItem2 hook', + ({ + render, + renderFromJSX, + treeItemComponentName, + TreeItemComponent, + treeViewComponentName, + TreeViewComponent, + }) => { + describe('role prop', () => { + it('should have the role="treeitem" on the root slot', () => { + const response = render({ items: [{ id: '1' }] }); + + expect(response.getItemRoot('1')).to.have.attribute('role', 'treeitem'); + }); + + it('should have the role "group" on the groupTransition slot if the item is expandable', () => { + const response = render({ + items: [{ id: '1', children: [{ id: '1.1' }] }], + defaultExpandedItems: ['1'], + }); + + expect( + response.getItemRoot('1').querySelector(`.${treeItemClasses.groupTransition}`), + ).to.have.attribute('role', 'group'); + }); + }); + + describe('onClick prop', () => { + it('should call onClick when clicked, but not when children are clicked for TreeItem', () => { + const onClick = spy(); + + const response = render({ + items: [{ id: '1', children: [{ id: '1.1' }] }], + defaultExpandedItems: ['1'], + slotProps: { + item: { + onClick, + }, + }, + }); + + fireEvent.click(response.getItemContent('1.1')); + expect(onClick.callCount).to.equal(treeItemComponentName === 'TreeItem' ? 1 : 2); + expect(onClick.lastCall.firstArg.target.parentElement.dataset.testid).to.equal('1.1'); + }); + + it('should call onClick even when the element is disabled', () => { + const onClick = spy(); + + const response = render({ + items: [{ id: '1', disabled: true }], + slotProps: { + item: { + onClick, + }, + }, + }); + + fireEvent.click(response.getItemContent('1')); + expect(onClick.callCount).to.equal(1); + }); + }); + + it('should be able to type in a child input', () => { + const response = render({ + items: [{ id: '1', children: [{ id: '1.1' }] }], + defaultExpandedItems: ['1'], + slotProps: + treeItemComponentName === 'TreeItem2' + ? { + item: { + slots: { + label: () => , + }, + }, + } + : { + item: { + label: , + }, + }, + }); + + const input = response.getItemRoot('1.1').querySelector('.icon-input')!; + const keydownEvent = createEvent.keyDown(input, { + key: 'a', + }); + + const handlePreventDefault = spy(); + keydownEvent.preventDefault = handlePreventDefault; + fireEvent(input, keydownEvent); + expect(handlePreventDefault.callCount).to.equal(0); + }); + + it('should not focus steal', () => { + let setActiveItemMounted; + // a TreeItem whose mounted state we can control with `setActiveItemMounted` + function ConditionallyMountedItem(props) { + const [mounted, setMounted] = React.useState(true); + if (props.itemId === '2') { + setActiveItemMounted = setMounted; + } + + if (!mounted) { + return null; + } + return ; + } + + let response: DescribeTreeViewRendererUtils; + if (treeViewComponentName === 'SimpleTreeView') { + response = renderFromJSX( + + + + + + + , + ); + } else { + response = renderFromJSX( + + + ({ 'data-testid': ownerState.itemId }) as any, + }} + getItemLabel={(item) => item.id} + /> + , + ); + } + + act(() => { + response.getItemRoot('2').focus(); + }); + + expect(response.getFocusedItemId()).to.equal('2'); + + act(() => { + screen.getByRole('button').focus(); + }); + + expect(screen.getByRole('button')).toHaveFocus(); + + act(() => { + setActiveItemMounted(false); + }); + act(() => { + setActiveItemMounted(true); + }); + + expect(screen.getByRole('button')).toHaveFocus(); + }); + }, +); diff --git a/test/utils/tree-view/describeTreeView/index.ts b/test/utils/tree-view/describeTreeView/index.ts index 4459a7a955838..d545fcba81745 100644 --- a/test/utils/tree-view/describeTreeView/index.ts +++ b/test/utils/tree-view/describeTreeView/index.ts @@ -1,2 +1,5 @@ export { describeTreeView } from './describeTreeView'; -export type { DescribeTreeViewRendererReturnValue } from './describeTreeView.types'; +export type { + DescribeTreeViewRendererReturnValue, + DescribeTreeViewRendererUtils, +} from './describeTreeView.types';