From 92028d86221667da8169ead559b578949d176b72 Mon Sep 17 00:00:00 2001 From: delangle Date: Wed, 24 Apr 2024 12:41:36 +0200 Subject: [PATCH 1/6] [core] Use describeTreeView for items test (partial) --- .../SimpleTreeView/SimpleTreeView.test.tsx | 48 ------- .../src/TreeItem/TreeItem.test.tsx | 87 ------------ .../useTreeViewExpansion.test.tsx | 4 +- .../useTreeViewItems.test.tsx | 124 +++++++++++++++++- .../describeTreeView/describeTreeView.tsx | 5 +- .../describeTreeView.types.ts | 6 +- 6 files changed, 131 insertions(+), 143 deletions(-) diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx index 1d18084b46f4..f1a74fc5a8f3 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.test.tsx @@ -166,27 +166,6 @@ describe('', () => { expect(getByTestId('two')).toHaveFocus(); }); - it('should support conditional rendered tree items', () => { - function TestComponent() { - const [hide, setState] = React.useState(false); - - return ( - - - {!hide && } - - ); - } - - const { getByText, queryByText } = render(); - - expect(getByText('test')).not.to.equal(null); - fireEvent.click(getByText('Hide')); - expect(queryByText('test')).to.equal(null); - }); - it('should work in a portal', () => { const { getByTestId } = render( @@ -215,33 +194,6 @@ describe('', () => { expect(getByTestId('four')).toHaveFocus(); }); - it('should update indexes when two items are swapped', () => { - const onSelectedItemsChange = spy(); - - function TestComponent() { - const [items, setItems] = React.useState(['1', '2', '3']); - - return ( - - - - {items.map((itemId) => ( - - ))} - - - ); - } - - const { getByText } = render(); - fireEvent.click(getByText('Swap items')); - fireEvent.click(getByText('1')); - fireEvent.click(getByText('3'), { shiftKey: true }); - expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1', '3']); - }); - describe('Accessibility', () => { it('(TreeView) should have the role `tree`', () => { const { getByRole } = render(); diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx index beebfefe762d..1624896acf1e 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx @@ -119,46 +119,6 @@ describe('', () => { expect(handleClick.callCount).to.equal(1); }); - it('should allow conditional child', () => { - function TestComponent() { - const [hide, setState] = React.useState(false); - - return ( - - - - - {!hide && } - - - - ); - } - const { getByTestId, queryByTestId } = render(); - - expect(getByTestId('1')).to.have.attribute('aria-expanded', 'true'); - expect(getByTestId('2')).not.to.equal(null); - fireEvent.click(getByTestId('button')); - expect(getByTestId('1')).not.to.have.attribute('aria-expanded'); - expect(queryByTestId('2')).to.equal(null); - }); - - it('should treat an empty array equally to no children', () => { - const { getByTestId } = render( - - - - {[]} - - - , - ); - - expect(getByTestId('2')).not.to.have.attribute('aria-expanded'); - }); - it('should treat multiple empty conditional arrays as empty', () => { const { getByTestId } = render( @@ -225,16 +185,6 @@ describe('', () => { expect(handleClick.callCount).to.equal(0); }); - it('should be able to use a custom id', () => { - const { getByRole } = render( - - - , - ); - - expect(getByRole('treeitem')).to.have.attribute('id', 'customId'); - }); - describe('Accessibility', () => { it('should have the role `treeitem`', () => { const { getByTestId } = render( @@ -258,28 +208,6 @@ describe('', () => { expect(getByRole('group')).to.contain(getByText('test2')); }); - describe('aria-disabled', () => { - it('should not have the attribute `aria-disabled` if disabled is false', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).not.to.have.attribute('aria-disabled'); - }); - - it('should have the attribute `aria-disabled={true}` if disabled', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-disabled', 'true'); - }); - }); - describe('Navigation', () => { describe('right arrow interaction', () => { it('should open the item and not move the focus if focus is on a closed item', () => { @@ -1694,21 +1622,6 @@ describe('', () => { expect(handleClick.callCount).to.equal(1); }); }); - - it('should disable child items when parent item is disabled', () => { - const { getByTestId } = render( - - - - - - , - ); - - expect(getByTestId('one')).to.have.attribute('aria-disabled', 'true'); - expect(getByTestId('two')).to.have.attribute('aria-disabled', 'true'); - expect(getByTestId('three')).to.have.attribute('aria-disabled', 'true'); - }); }); describe('content customisation', () => { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx index f0b18b58c1f5..a053e6af6274 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx @@ -22,7 +22,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>( }); expect(response.isItemExpanded('1')).to.equal(false); - expect(response.getAllItemRoots()).to.have.length(2); + expect(response.getAllItemId()).to.deep.equal(['1', '2']); }); it('should use the default state when defined', () => { @@ -32,7 +32,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>( }); expect(response.isItemExpanded('1')).to.equal(true); - expect(response.getAllItemRoots()).to.have.length(3); + expect(response.getAllItemId()).to.deep.equal(['1', '1.1', '2']); }); it('should use the controlled state when defined', () => { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index 98e161d10f2c..081fa0028159 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -1,7 +1,16 @@ import { expect } from 'chai'; +import { spy } from 'sinon'; +import { fireEvent } from '@mui-internal/test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; +import { + UseTreeViewExpansionSignature, + UseTreeViewItemsSignature, + UseTreeViewSelectionSignature, +} from '@mui/x-tree-view/internals'; -describeTreeView('useTreeViewItems plugin', ({ render, treeViewComponent }) => { +describeTreeView< + [UseTreeViewItemsSignature, UseTreeViewExpansionSignature, UseTreeViewSelectionSignature] +>('useTreeViewItems plugin', ({ render, treeViewComponent }) => { it('should throw an error when two items have the same ID', function test() { // TODO is this fixed? if (!/jsdom/.test(window.navigator.userAgent)) { @@ -21,4 +30,117 @@ describeTreeView('useTreeViewItems plugin', ({ render, treeViewComponent }) => { ], ); }); + + it('should be able to use a custom id attribute', function test() { + // For now, only SimpleTreeView can use custom id attributes + if (treeViewComponent.startsWith('RichTreeView')) { + this.skip(); + } + + const response = render({ + items: [{ id: '1' }], + slotProps: { + item: { + id: 'customId', + }, + }, + }); + + expect(response.getItemRoot('1')).to.have.attribute('id', 'customId'); + }); + + describe('items prop / JSX Tree Item', () => { + it('should support removing an item', () => { + const response = render({ + items: [{ id: '1' }, { id: '2' }], + }); + + response.setItems([{ id: '1' }]); + expect(response.getAllItemId()).to.deep.equal(['1']); + }); + + it('should support adding an item at the end', () => { + const response = render({ + items: [{ id: '1' }], + }); + + response.setItems([{ id: '1' }, { id: '2' }]); + expect(response.getAllItemId()).to.deep.equal(['1', '2']); + }); + + it('should support adding an item at the beginning', () => { + const response = render({ + items: [{ id: '2' }], + }); + + response.setItems([{ id: '1' }, { id: '2' }]); + expect(response.getAllItemId()).to.deep.equal(['1', '2']); + }); + + it('should update indexes when two items are swapped', () => { + const onSelectedItemsChange = spy(); + + const response = render({ + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + multiSelect: true, + onSelectedItemsChange, + }); + + response.setItems([{ id: '1' }, { id: '3' }, { id: '2' }]); + expect(response.getAllItemId()).to.deep.equal(['1', '3', '2']); + + // Check if the internal state is updates by running a range selection + fireEvent.click(response.getItemContent('1')); + fireEvent.click(response.getItemContent('3'), { shiftKey: true }); + expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1', '3']); + }); + + it('should not mark an item as expandable its children are an empty array', () => { + const response = render({ + items: [{ id: '1', children: [] }], + defaultExpandedItems: ['1'], + }); + + expect(response.getItemRoot('1')).not.to.have.attribute('aria-expanded'); + }); + }); + + describe('disabled prop', () => { + it('should not have the attribute `aria-disabled` if disabled is not defined', () => { + const response = render({ + items: [{ id: '1' }], + }); + + expect(response.getItemRoot('1')).not.to.have.attribute('aria-disabled'); + }); + + it('should not have the attribute `aria-disabled` if disabled is false', () => { + const response = render({ + items: [{ id: '1', disabled: false }], + }); + + expect(response.getItemRoot('1')).not.to.have.attribute('aria-disabled'); + }); + + it('should have the attribute `aria-disabled` if disabled is true', () => { + const response = render({ + items: [{ id: '1', disabled: true }], + }); + + expect(response.getItemRoot('1')).to.have.attribute('aria-disabled'); + }); + + it('should disable all descendants of a disabled item', () => { + const response = render({ + items: [ + { id: '1', disabled: true, children: [{ id: '1.1', children: [{ id: '1.1.1' }] }] }, + ], + defaultExpandedItems: ['1', '1.1'], + }); + + expect(response.getItemRoot('1')).to.have.attribute('aria-disabled', 'true'); + expect(response.getItemRoot('1.1')).to.have.attribute('aria-disabled', 'true'); + expect(response.getItemRoot('1.1.1')).to.have.attribute('aria-disabled', 'true'); + }); + }); }); diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index 6cfccabee847..099515afa58b 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -26,7 +26,8 @@ const innerDescribeTreeView = ( ): Omit, 'setProps' | 'setItems' | 'apiRef'> => { const getRoot = () => result.getByRole('tree'); - const getAllItemRoots = () => result.queryAllByRole('treeitem'); + const getAllItemId = () => + result.queryAllByRole('treeitem').map((item) => item.dataset.testid!); const getFocusedItemId = () => { const activeElement = document.activeElement; @@ -54,7 +55,7 @@ const innerDescribeTreeView = ( return { getRoot, - getAllItemRoots, + getAllItemId, getFocusedItemId, getItemRoot, getItemContent, diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index ba15ccee9e59..a77ee077c549 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -40,10 +40,10 @@ export interface DescribeTreeViewRendererReturnValue< */ getFocusedItemId: () => string | null; /** - * Returns the `root` slot of all the items. - * @returns {HTMLElement[]} List of the `root` slot of all the items. + * Returns the item id of all the items currently rendered. + * @returns {HTMLElement[]} List of the item id of all the items currently rendered. */ - getAllItemRoots: () => HTMLElement[]; + getAllItemId: () => string[]; /** * Returns the `root` slot of the item with the given id. * @param {string} id The id of the item to retrieve. From db4ae468fce759979a2b8547ea363f83dbad42a9 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Thu, 16 May 2024 12:13:41 +0200 Subject: [PATCH 2/6] Update packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx Co-authored-by: Nora <72460825+noraleonte@users.noreply.github.com> Signed-off-by: Flavien DELANGLE --- .../plugins/useTreeViewItems/useTreeViewItems.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index 081fa0028159..5edef1e3db16 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -95,7 +95,7 @@ describeTreeView< expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1', '3']); }); - it('should not mark an item as expandable its children are an empty array', () => { + it('should not mark an item as expandable if its children are an empty array', () => { const response = render({ items: [{ id: '1', children: [] }], defaultExpandedItems: ['1'], From 044f3d202bc9ec26c5335de5e66b5516abdb83e6 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Fri, 17 May 2024 12:26:46 +0200 Subject: [PATCH 3/6] Update packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx Co-authored-by: Lukas Signed-off-by: Flavien DELANGLE --- .../plugins/useTreeViewItems/useTreeViewItems.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index 5edef1e3db16..e2c7f8f3a405 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -89,7 +89,7 @@ describeTreeView< response.setItems([{ id: '1' }, { id: '3' }, { id: '2' }]); expect(response.getAllItemId()).to.deep.equal(['1', '3', '2']); - // Check if the internal state is updates by running a range selection + // Check if the internal state is updated by running a range selection fireEvent.click(response.getItemContent('1')); fireEvent.click(response.getItemContent('3'), { shiftKey: true }); expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1', '3']); From ceb6da856f48af1cb4683c3dc3adbdca4a3c224b Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Fri, 17 May 2024 12:26:51 +0200 Subject: [PATCH 4/6] Update packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx Co-authored-by: Lukas Signed-off-by: Flavien DELANGLE --- .../plugins/useTreeViewItems/useTreeViewItems.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index e2c7f8f3a405..7d4367176ab6 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -95,7 +95,7 @@ describeTreeView< expect(onSelectedItemsChange.lastCall.args[1]).to.deep.equal(['1', '3']); }); - it('should not mark an item as expandable if its children are an empty array', () => { + it('should not mark an item as expandable if its children is an empty array', () => { const response = render({ items: [{ id: '1', children: [] }], defaultExpandedItems: ['1'], From 9b871d6a196c6ce72fbb9e24843bcc669cd264f8 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 17 May 2024 12:28:08 +0200 Subject: [PATCH 5/6] Code review: Lukas --- .../useTreeViewExpansion/useTreeViewExpansion.test.tsx | 4 ++-- .../plugins/useTreeViewItems/useTreeViewItems.test.tsx | 8 ++++---- .../utils/tree-view/describeTreeView/describeTreeView.tsx | 4 ++-- .../tree-view/describeTreeView/describeTreeView.types.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx index a053e6af6274..f7dc38a29057 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx @@ -22,7 +22,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>( }); expect(response.isItemExpanded('1')).to.equal(false); - expect(response.getAllItemId()).to.deep.equal(['1', '2']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1', '2']); }); it('should use the default state when defined', () => { @@ -32,7 +32,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>( }); expect(response.isItemExpanded('1')).to.equal(true); - expect(response.getAllItemId()).to.deep.equal(['1', '1.1', '2']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1', '1.1', '2']); }); it('should use the controlled state when defined', () => { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index 7d4367176ab6..c39baad16328 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -56,7 +56,7 @@ describeTreeView< }); response.setItems([{ id: '1' }]); - expect(response.getAllItemId()).to.deep.equal(['1']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1']); }); it('should support adding an item at the end', () => { @@ -65,7 +65,7 @@ describeTreeView< }); response.setItems([{ id: '1' }, { id: '2' }]); - expect(response.getAllItemId()).to.deep.equal(['1', '2']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1', '2']); }); it('should support adding an item at the beginning', () => { @@ -74,7 +74,7 @@ describeTreeView< }); response.setItems([{ id: '1' }, { id: '2' }]); - expect(response.getAllItemId()).to.deep.equal(['1', '2']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1', '2']); }); it('should update indexes when two items are swapped', () => { @@ -87,7 +87,7 @@ describeTreeView< }); response.setItems([{ id: '1' }, { id: '3' }, { id: '2' }]); - expect(response.getAllItemId()).to.deep.equal(['1', '3', '2']); + expect(response.getAllTreeItemIds()).to.deep.equal(['1', '3', '2']); // Check if the internal state is updated by running a range selection fireEvent.click(response.getItemContent('1')); diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index 8cc18c8be81a..763cd8ee7d5d 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -26,7 +26,7 @@ const innerDescribeTreeView = ( ): Omit, 'setProps' | 'setItems' | 'apiRef'> => { const getRoot = () => result.getByRole('tree'); - const getAllItemId = () => + const getAllTreeItemIds = () => result.queryAllByRole('treeitem').map((item) => item.dataset.testid!); const getFocusedItemId = () => { @@ -61,7 +61,7 @@ const innerDescribeTreeView = ( return { getRoot, - getAllItemId, + getAllTreeItemIds, getFocusedItemId, getItemRoot, getItemContent, diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index 7a44c1f0716f..6b21548e8c60 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -43,7 +43,7 @@ export interface DescribeTreeViewRendererReturnValue< * Returns the item id of all the items currently rendered. * @returns {HTMLElement[]} List of the item id of all the items currently rendered. */ - getAllItemId: () => string[]; + getAllTreeItemIds: () => string[]; /** * Returns the `root` slot of the item with the given id. * @param {string} id The id of the item to retrieve. From 25b0c4fb53564b20af6aa699c800a72185f18ef5 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 17 May 2024 12:29:43 +0200 Subject: [PATCH 6/6] Code review: Lukas --- .../useTreeViewItems.test.tsx | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index c39baad16328..c436e063cd77 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -108,26 +108,12 @@ describeTreeView< describe('disabled prop', () => { it('should not have the attribute `aria-disabled` if disabled is not defined', () => { const response = render({ - items: [{ id: '1' }], - }); - - expect(response.getItemRoot('1')).not.to.have.attribute('aria-disabled'); - }); - - it('should not have the attribute `aria-disabled` if disabled is false', () => { - const response = render({ - items: [{ id: '1', disabled: false }], + items: [{ id: '1' }, { id: '2', disabled: false }, { id: '3', disabled: true }], }); expect(response.getItemRoot('1')).not.to.have.attribute('aria-disabled'); - }); - - it('should have the attribute `aria-disabled` if disabled is true', () => { - const response = render({ - items: [{ id: '1', disabled: true }], - }); - - expect(response.getItemRoot('1')).to.have.attribute('aria-disabled'); + expect(response.getItemRoot('2')).not.to.have.attribute('aria-disabled'); + expect(response.getItemRoot('3')).to.have.attribute('aria-disabled'); }); it('should disable all descendants of a disabled item', () => {