diff --git a/cypress/component/Drilldown.cy.tsx b/cypress/component/Drilldown.cy.tsx new file mode 100644 index 0000000000..e2163c607b --- /dev/null +++ b/cypress/component/Drilldown.cy.tsx @@ -0,0 +1,624 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import 'cypress-real-events' + +import { Drilldown } from '../../packages/ui' +import '../support/component' + +const data = Array(5) + .fill(0) + .map((_v, ind) => ({ + label: `option ${ind}`, + id: `opt_${ind}` + })) + +const renderOptions = (page: string) => { + return data.map((option) => ( + + {option.label} - {page} + + )) +} + +describe('', () => { + it('should disabled prop prevent option actions', async () => { + cy.mount( + + + + Option-0 + + + + Option-1 + + + ) + cy.contains('Option-0').realClick() + + cy.get('Option-0').should('not.exist') + cy.contains('Option-1').should('be.visible') + }) + + it('should disabled trigger, if disabled prop provided', async () => { + cy.mount( + Toggle} + > + + Option + + + ) + + cy.get('[data-test-id="toggleButton"]') + .should('have.attr', 'aria-disabled', 'true') + .and('be.disabled') + + cy.get('[data-test-id="toggleButton"]').click({ force: true }) + + cy.get('#page0option').should('not.exist') + }) + + it('should rotate focus in the drilldown by default', async () => { + cy.mount( + + + Option1 + Option2 + + + ) + cy.get('body').realClick() + cy.get('body').realPress('Tab') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option01') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option02') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option01') + }) + + it('should prevent focus rotation in the drilldown with "false"', async () => { + cy.mount( + + + Option + Option + + + ) + cy.get('body').realClick() + cy.get('body').realPress('Tab') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option01') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option02') + + cy.realPress('ArrowDown') + cy.focused().should('have.id', 'option02') + }) + + it('should set the width of the drilldown', async () => { + cy.mount( + + + Option + + + ) + cy.get('[role="menu"]').should('have.css', 'width', '320px') + }) + + it('should set the width of the drilldown in the popover', async () => { + cy.mount( + Toggle} + defaultShow + > + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'width', + '320px' + ) + }) + + it('should be overruled by maxWidth prop', async () => { + cy.mount( + + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'width', + '160px' + ) + }) + + it('should be affected by overflowX prop', async () => { + cy.mount( + + + + + Option with a very long label so that it has to break + + + + + ) + cy.get('[class$="-drilldown__container"]') + .should('have.css', 'width', '320px') + .and('have.css', 'overflow-x', 'auto') + .then(($container) => { + const scrollWidth = $container[0].scrollWidth + const clientWidth = $container[0].clientWidth + + cy.wrap(scrollWidth > clientWidth).should('be.true') + }) + }) + + it('should set minWidth in popover mode', async () => { + cy.mount( + Trigger} + show + onToggle={cy.spy()} + > + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'width', + '336px' + ) + }) + + it('should set the height of the drilldown', async () => { + cy.mount( + + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'height', + '320px' + ) + }) + + it('should set the height of the drilldown in the popover', async () => { + cy.mount( + Toggle} + defaultShow + > + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'height', + '320px' + ) + }) + + it('should be overruled by maxHeight prop', async () => { + cy.mount( + + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'height', + '160px' + ) + }) + + it('should be affected by overflowY prop', async () => { + cy.mount( + + + Option + Option + Option + Option + Option + Option + + + ) + cy.get('[class$="-drilldown__container"]') + .should('have.css', 'height', '160px') + .and('have.css', 'overflow-y', 'auto') + .then(($container) => { + const scrollHeight = $container[0].scrollHeight + const clientHeight = $container[0].clientHeight + + cy.wrap(scrollHeight > clientHeight).should('be.true') + }) + }) + + it('should minHeight prop set height', async () => { + cy.mount( + + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'height', + '336px' + ) + }) + + it('should minHeight prop set height in popover mode', async () => { + cy.mount( + Trigger} + show + onToggle={cy.spy()} + > + + Option + + + ) + cy.get('[class$="-drilldown__container"]').should( + 'have.css', + 'height', + '336px' + ) + }) + + it('should call onDismiss when Drilldown is closed', async () => { + const onDismiss = cy.spy() + cy.mount( + Options} + onDismiss={onDismiss} + defaultShow + > + + Option 0 + + + ) + cy.get('div[role="menu"]').focus() + + cy.realPress('Escape') + + cy.wrap(onDismiss) + .should('have.been.called') + .and( + 'have.been.calledWithMatch', + Cypress.sinon.match.instanceOf(Event), + false + ) + }) + + it('should shouldHideOnSelect prop be true by default', async () => { + cy.mount( + Toggle} + defaultShow + > + + Option-01 + + + ) + cy.get('#option01').should('exist') + cy.get('#option01').click() + cy.get('#option01').should('not.exist') + }) + + it('should not close on subPage nav, even if shouldHideOnSelect is "true"', async () => { + cy.mount( + Toggle} + defaultShow + shouldHideOnSelect={true} + > + + + Option + + + + Sub-Option + + + ) + cy.get('#option01').should('exist') + cy.get('#option11').should('not.exist') + + cy.get('#option01').click() + + cy.get('#option01').should('not.exist') + cy.get('#option11').should('exist') + }) + + it('should not close on Back nav, even if shouldHideOnSelect is "true"', async () => { + cy.mount( + Toggle} + defaultShow + shouldHideOnSelect={true} + > + + + Option01 + + + + Sub-Option + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Option01').should('not.exist') + cy.contains('Sub-Option').should('be.visible') + + cy.contains('Back').click() + + cy.contains('Sub-Option').should('not.exist') + cy.contains('Option01').should('be.visible') + }) + + it('should prevent closing when shouldHideOnSelect is "false"', async () => { + cy.mount( + Toggle} + defaultShow + shouldHideOnSelect={false} + > + + Option01 + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Option01').should('be.visible') + }) + + it('should be able to navigate between options with up/down arrows', async () => { + cy.mount( + + + {data.map((option) => ( + + {option.label} + + ))} + + + ) + cy.get('div[role="menu"]').focus() + + cy.realPress('ArrowDown') + cy.get('#opt_0').should('have.focus') + + cy.realPress('ArrowDown') + cy.get('#opt_1').should('have.focus') + + cy.realPress('ArrowDown') + cy.get('#opt_2').should('have.focus') + + cy.realPress('ArrowUp') + cy.get('#opt_1').should('have.focus') + }) + + it('should be able to navigate forward between pages with right arrow', async () => { + cy.mount( + + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + cy.get('div[role="menu"]').focus() + + // the option which navigates to next page should be focused + cy.realPress('ArrowDown') + cy.get('#opt0').should('have.focus').and('have.text', 'To Page 1') + + // go to Page 1 + cy.realPress('ArrowRight') + + // on the Page 1 the 1st option is the `Back` button + cy.realPress('ArrowDown') + cy.contains('[role="menuitem"]', 'Back').should('have.focus') + + // next arrowDown should skip the header Title and focus on 'To Page 2' option + cy.realPress('ArrowDown') + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 1').should('be.visible') + cy.contains('[role="button"]', 'To Page 2').should('have.focus') + + // go to Page 2 + cy.realPress('ArrowRight') + + // on Page 2 the header title should be 'Page 2' + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 2').should('be.visible') + }) + + it('should be able to navigate back to previous page with left arrow', async () => { + cy.mount( + + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + cy.get('div[role="menu"]').focus() + + // go to Page 1 + cy.realPress('ArrowDown') + cy.realPress('ArrowRight') + + // on Page 1 should be visible header title + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 1').should('be.visible') + + // go to Page 0 + cy.realPress('ArrowLeft') + + // on Page 0 should be visible header title + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('be.visible') + }) + + it('should close the drilldown on root page and left arrow is pressed', async () => { + cy.mount( + options} + defaultShow + > + + + To Page 1 + + + + {[ + + To Page 2 + , + ...renderOptions('page 1') + ]} + + + + {renderOptions('page 2')} + + + ) + cy.get('div[role="menu"]').focus() + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('be.visible') + + cy.realPress('ArrowLeft') + + cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 0').should('not.exist') + cy.contains('div[role="menu"]').should('not.exist') + }) + + it('should correctly return focus when "trigger" and "shouldReturnFocus" is set', async () => { + cy.mount( + Options} + shouldReturnFocus + > + + Option-0 + + + ) + cy.contains('button', 'Options').focus() + cy.contains('Option-0').should('not.exist') + + cy.realPress('Space') + + cy.contains('Option-0').should('be.visible') + + cy.realPress('Escape') + + cy.contains('Option-0').should('not.exist') + cy.contains('button', 'Options').should('have.focus') + }) +}) diff --git a/cypress/component/DrilldownGroup.cy.tsx b/cypress/component/DrilldownGroup.cy.tsx new file mode 100644 index 0000000000..d1c19f3415 --- /dev/null +++ b/cypress/component/DrilldownGroup.cy.tsx @@ -0,0 +1,127 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import 'cypress-real-events' + +import { Drilldown } from '../../packages/ui' +import '../support/component' + +function mountDrilldown(selectableType, defaultSelected) { + cy.mount( + + + + + Option0 + + + Option1 + + + Option2 + + + + + ) +} + +describe('', () => { + it('should toggle the selected option only when selectableType is multiple', async () => { + const selectedValues = ['item0', 'item1', 'item2'] + const selectableType = 'multiple' + + mountDrilldown(selectableType, selectedValues) + + cy.get('[role="menuitemcheckbox"]').each(($option) => { + cy.wrap($option).should('have.attr', 'aria-checked', 'true') + }) + + cy.get('[role="menuitemcheckbox"]').eq(1).realClick() + + cy.get('[role="menuitemcheckbox"]') + .eq(0) + .should('have.attr', 'aria-checked', 'true') + cy.get('[role="menuitemcheckbox"]') + .eq(1) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitemcheckbox"]') + .eq(2) + .should('have.attr', 'aria-checked', 'true') + }) + + it('should toggle options in radio fashion when selectableType is single', async () => { + const selectedValues = ['item0'] + const selectableType = 'single' + + mountDrilldown(selectableType, selectedValues) + + cy.get('[role="menuitemradio"]') + .eq(0) + .should('have.attr', 'aria-checked', 'true') + cy.get('[role="menuitemradio"]') + .eq(1) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitemradio"]') + .eq(2) + .should('have.attr', 'aria-checked', 'false') + + cy.get('[role="menuitemradio"]').eq(1).realClick() + + cy.get('[role="menuitemradio"]') + .eq(0) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitemradio"]') + .eq(1) + .should('have.attr', 'aria-checked', 'true') + cy.get('[role="menuitemradio"]') + .eq(2) + .should('have.attr', 'aria-checked', 'false') + }) + + it('should themeOverride prop passed to the Options component', async () => { + cy.mount( + + + + Option + Option + + + + ) + + cy.contains('[role="presentation"]', 'Group label') + .should('exist') + .and('have.css', 'color', 'rgb(100, 0, 0)') + }) +}) diff --git a/cypress/component/DrilldownOption.cy.tsx b/cypress/component/DrilldownOption.cy.tsx new file mode 100644 index 0000000000..e975291960 --- /dev/null +++ b/cypress/component/DrilldownOption.cy.tsx @@ -0,0 +1,304 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import 'cypress-real-events' + +import { Drilldown } from '../../packages/ui' +import '../support/component' + +describe('', () => { + it('should allow controlled behaviour', async () => { + const options = ['one', 'two', 'three'] + const Example = ({ + opts, + selected + }: { + opts: typeof options + selected: string + }) => { + return ( + + + + {opts.map((opt) => { + return ( + + {opt} + + ) + })} + + + + ) + } + cy.mount() + + cy.get('[role="menuitem"]') + .eq(0) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitem"]') + .eq(1) + .should('have.attr', 'aria-checked', 'true') + cy.get('[role="menuitem"]') + .eq(2) + .should('have.attr', 'aria-checked', 'false') + + cy.mount() + + cy.get('[role="menuitem"]') + .eq(0) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitem"]') + .eq(1) + .should('have.attr', 'aria-checked', 'false') + cy.get('[role="menuitem"]') + .eq(2) + .should('have.attr', 'aria-checked', 'true') + }) + + it('should navigate to subPage on select', async () => { + cy.mount( + + + + Option01 + + + + Sub-Option + + + ) + cy.contains('Sub-Option').should('not.exist') + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Sub-Option').should('be.visible') + cy.contains('Option01').should('not.exist') + }) + + it('should disabled prop apply disabled css style', async () => { + cy.mount( + + + + Option + + + + ) + cy.get('#option1') + .should('have.attr', 'aria-disabled', 'true') + .and('have.css', 'cursor', 'not-allowed') + }) + + it('should navigate to url on Focus + Space', async () => { + cy.mount( + + + + Option + + + + ) + cy.get('#option1').focus().realPress('Space') + + cy.url().should('include', '#helloWorld') + }) + + it('should navigate to url on Click', async () => { + cy.mount( + + + + Option + + + + ) + cy.get('#option1').realClick() + cy.url().should('include', '#helloWorld') + }) + + it("shouldn't navigate to url, if disabled", async () => { + cy.mount( + + + + Option + + + + ) + cy.get('#option1').realClick() + + cy.url().should('not.include', '#helloWorld') + }) + + it('should renderLabelInfo prop affected by afterLabelContentVAlign prop', async () => { + cy.mount( + + + + Option + + + + ) + cy.contains('[class$=-drilldown__optionLabelInfo]', 'Info').should( + 'have.css', + 'align-self', + 'flex-end' + ) + }) + + it('should provide goToPreviousPage method that goes back to the previous page', async () => { + cy.mount( + + + + Option01 + + + + + { + goToPreviousPage() + }} + > + Option11 + + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Option01').should('not.exist') + cy.contains('Option11').should('be.visible') + + cy.get('#option11').click() + + cy.contains('Option01').should('be.visible') + cy.contains('Option11').should('not.exist') + }) + + it('should provide goToPage method that can be used to go back a page', async () => { + cy.mount( + + + + Option01 + + + + + { + goToPage(pageHistory[0]) + }} + > + Option11 + + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Option01').should('not.exist') + cy.contains('Option11').should('be.visible') + + cy.get('#option11').click() + + cy.contains('Option01').should('be.visible') + cy.contains('Option11').should('not.exist') + }) + + it('should provide goToPage method that can be used to go to a new, existing page', async () => { + cy.mount( + + + { + goToPage('page1') + }} + > + Option01 + + + + + Option11 + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option01').click() + + cy.contains('Option01').should('not.exist') + cy.contains('Option11').should('be.visible') + }) + + it('should themeOverride prop passed to the Options.Item component', async () => { + cy.mount( + + + + Option01 + + + + ) + cy.contains('li[class$="-optionItem"]', 'Option01') + .should('have.css', 'color', 'rgb(0, 0, 100)') + .and('have.css', 'backgroundColor', 'rgb(200, 200, 200)') + }) +}) diff --git a/cypress/component/DrilldownPage.cy.tsx b/cypress/component/DrilldownPage.cy.tsx new file mode 100644 index 0000000000..9a36dc7c39 --- /dev/null +++ b/cypress/component/DrilldownPage.cy.tsx @@ -0,0 +1,116 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import 'cypress-real-events' + +import { Drilldown } from '../../packages/ui' +import '../support/component' + +describe('', () => { + it('should have a back arrow in header back navigation', async () => { + cy.mount( + + + + Option01 + + + + Option22 + + + ) + + cy.get('#option1').click() + + cy.contains('li[class$="-optionItem"]', 'HeaderBackString') + .find('svg[name="IconArrowOpenStart"]') + .should('exist') + }) + + it('should still display the back icon in header back navigation, even if function has no return value', async () => { + cy.mount( + + + + Option01 + + + null}> + Option22 + + + ) + cy.get('#option1').click() + + cy.get('div[role="menu"]') + .find('svg[name="IconArrowOpenStart"]') + .should('exist') + }) + + it('should fire onBackButtonClicked on header back navigation click', async () => { + const backNavCallback = cy.spy() + cy.mount( + + + + Option01 + + + + Option22 + + + ) + cy.get('#option1').click() + + cy.contains('li[class$="-optionItem"]', 'Back').click() + + cy.wrap(backNavCallback).should('have.been.called') + }) + + it('should go back one page on click', async () => { + cy.mount( + + + + Option01 + + + + Option22 + + + ) + cy.contains('Option01').should('be.visible') + + cy.get('#option1').click() + + cy.contains('Option01').should('not.exist') + + cy.contains('li[class$="-optionItem"]', 'Back').click() + + cy.contains('Option01').should('be.visible') + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/locator.ts b/cypress/component/DrilldownSeparator.cy.tsx similarity index 63% rename from packages/ui-drilldown/src/Drilldown/locator.ts rename to cypress/component/DrilldownSeparator.cy.tsx index 1cddf4c022..91261540e9 100644 --- a/packages/ui-drilldown/src/Drilldown/locator.ts +++ b/cypress/component/DrilldownSeparator.cy.tsx @@ -21,8 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import React from 'react' +import 'cypress-real-events' -import { DrilldownLocator } from './DrilldownLocator' +import { Drilldown } from '../../packages/ui' +import '../support/component' -export { DrilldownLocator } -export default DrilldownLocator +describe('', () => { + it('themeOverride prop should pass overrides to Option.Separator', async () => { + cy.mount( + + + + + + ) + cy.get('#separator1') + .should('have.css', 'height', '16px') + .and('have.css', 'backgroundColor', 'rgb(0, 128, 0)') + }) +}) diff --git a/package-lock.json b/package-lock.json index 2e81106517..87fabc3ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42905,16 +42905,73 @@ "prop-types": "^15.8.1" }, "devDependencies": { + "@instructure/ui-axe-check": "10.2.0", "@instructure/ui-babel-preset": "10.2.0", "@instructure/ui-color-utils": "10.2.0", - "@instructure/ui-test-locator": "10.2.0", + "@instructure/ui-scripts": "10.2.0", "@instructure/ui-test-utils": "10.2.0", - "@instructure/ui-themes": "10.2.0" + "@instructure/ui-themes": "10.2.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.5.2", + "vitest": "^2.0.2" }, "peerDependencies": { "react": ">=16.8 <=18" } }, + "packages/ui-drilldown/node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "packages/ui-drilldown/node_modules/@testing-library/react": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^10.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "packages/ui-drilldown/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "packages/ui-editable": { "name": "@instructure/ui-editable", "version": "10.2.0", diff --git a/packages/ui-drilldown/package.json b/packages/ui-drilldown/package.json index 83bd623466..8a8d3986e1 100644 --- a/packages/ui-drilldown/package.json +++ b/packages/ui-drilldown/package.json @@ -41,11 +41,16 @@ "prop-types": "^15.8.1" }, "devDependencies": { + "@instructure/ui-axe-check": "10.2.0", "@instructure/ui-babel-preset": "10.2.0", "@instructure/ui-color-utils": "10.2.0", - "@instructure/ui-test-locator": "10.2.0", + "@instructure/ui-scripts": "10.2.0", "@instructure/ui-test-utils": "10.2.0", - "@instructure/ui-themes": "10.2.0" + "@instructure/ui-themes": "10.2.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.5.2", + "vitest": "^2.0.2" }, "peerDependencies": { "react": ">=16.8 <=18" diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__new-tests__/DrilldownGroup.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__new-tests__/DrilldownGroup.test.tsx new file mode 100644 index 0000000000..2f8dffc920 --- /dev/null +++ b/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__new-tests__/DrilldownGroup.test.tsx @@ -0,0 +1,634 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { vi } from 'vitest' + +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' + +import { Drilldown } from '../../index' + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + // Mocking console to prevent test output pollution and expect for messages + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it('should not allow de-selecting an option when selectableType = "single"', async () => { + const options = ['one', 'two', 'three'] + const Example = ({ opts }: { opts: typeof options }) => { + return ( + + + + {opts.map((opt) => { + return ( + + {opt} + + ) + })} + + + + ) + } + + render() + + const selectedOption = screen.getByTestId('three') + + expect(selectedOption).toHaveAttribute('aria-checked', 'true') + + await userEvent.click(selectedOption) + + await waitFor(() => { + const updatedSelectedOption = screen.getByTestId('three') + + expect(updatedSelectedOption).toHaveAttribute('aria-checked', 'true') + }) + }) + + it("shouldn't render non-DrilldownGroup children", async () => { + render( + + + + Div + + + + ) + const nonGroupChild = screen.queryByTestId('testDiv') + const childText = screen.queryByText('Div') + + expect(nonGroupChild).not.toBeInTheDocument() + expect(childText).not.toBeInTheDocument() + }) + + it('elementRef should return ref to the group', async () => { + const elementRef = vi.fn() + const { container } = render( + + + + Option + + + + ) + const group = container.querySelector('#group0') + + expect(elementRef).toHaveBeenCalledWith(group) + }) + + describe('renderGroupTitle', () => { + it('should display', async () => { + render( + + + + Option + + + + ) + const title = screen.getByText('Group Title') + + expect(title).toBeInTheDocument() + }) + + it('should display, if function is provided', async () => { + render( + + + 'Group Title'}> + Option + + + + ) + const title = screen.getByText('Group Title') + + expect(title).toBeInTheDocument() + }) + }) + + describe('separators', () => { + it("shouldn't display, when group is first and/or last item", async () => { + const { container } = render( + + + + Option + + + + ) + const separators = container.querySelectorAll('[class$="-separator"]') + + expect(separators.length).toBe(0) + }) + + it('should display by default, if group is between other items', async () => { + const { container } = render( + + + Option + + Option + + Option + + + ) + const separators = container.querySelectorAll('[class$="-separator"]') + + expect(separators.length).toBe(2) + }) + + it("shouldn't display extra separator between groups", async () => { + const { container } = render( + + + Option + + Option + + + Option + + Option + + + ) + const separators = container.querySelectorAll('[class$="-separator"]') + + expect(separators.length).toBe(3) + }) + }) + + describe('disabled prop', () => { + it('should disable all items in the group', async () => { + const { container } = render( + + + Option + + Option + Option + + Option + + + ) + const disabledOption_1 = container.querySelector('#groupOption01') + const disabledOption_2 = container.querySelector('#groupOption02') + const option_1 = container.querySelector('#option1') + const option_2 = container.querySelector('#option2') + + expect(disabledOption_1).toHaveAttribute('aria-disabled', 'true') + expect(disabledOption_2).toHaveAttribute('aria-disabled', 'true') + expect(option_1).not.toHaveAttribute('aria-disabled') + expect(option_2).not.toHaveAttribute('aria-disabled') + }) + }) + + describe('role prop', () => { + it('should be "group" by default', async () => { + const { container } = render( + + + + Option + Option + + + + ) + const group = container.querySelector('#group0') + + expect(group).toHaveAttribute('role', 'group') + }) + + it('should render the group with passed role', async () => { + const { container } = render( + + + + Option + Option + + + + ) + const group = container.querySelector('#group0') + + expect(group).toHaveAttribute('role', 'menu') + }) + }) + + describe('as prop', () => { + it('should inherit Drilldown\'s "ul" by default', async () => { + const { container } = render( + + + + Option + Option + + + + ) + const drilldownContainer = container.querySelector( + '[id^=Selectable_][id$=-list]' + )! + const group = container.querySelector('#group0')! + + expect(drilldownContainer.tagName).toBe('UL') + expect(group.tagName).toBe('UL') + }) + + it('should inherit Drilldown\'s "as" prop', async () => { + const { container } = render( + + + + Option + Option + + + + ) + const drilldownContainer = container.querySelector( + '[id^=Selectable_][id$=-list]' + )! + const group = container.querySelector('#group0')! + + expect(drilldownContainer.tagName).toBe('OL') + expect(group.tagName).toBe('OL') + }) + + it('should render the group as other element', async () => { + const { container } = render( + + + + Option + Option + + + + ) + const drilldownContainer = container.querySelector( + '[id^=Selectable_][id$=-list]' + )! + const group = container.querySelector('#group0')! + + expect(drilldownContainer.tagName).toBe('UL') + expect(group.tagName).toBe('OL') + }) + }) + + describe('selectableType', () => { + it('if not set, should render role="menuitem" options without icon by default', async () => { + const { container } = render( + + + + Option + + + + ) + const icon = container.querySelector('svg[name="IconCheck"]') + const groupOption = container.querySelector('#groupOption01') + + expect(icon).not.toBeInTheDocument() + expect(groupOption).toHaveAttribute('role', 'menuitem') + }) + + it('value "single" should render role="menuitemradio" options with icon', async () => { + const { container } = render( + + + + Option + + + + ) + const icon = container.querySelector('svg[name="IconCheck"]') + const groupOption = container.querySelector('#groupOption01') + + expect(icon).toBeInTheDocument() + expect(groupOption).toHaveAttribute('role', 'menuitemradio') + }) + + it('value "multiple" should render role="menuitemcheckbox" options with icon', async () => { + const { container } = render( + + + + Option + + + + ) + const icon = container.querySelector('svg[name="IconCheck"]') + const groupOption = container.querySelector('#groupOption01') + + expect(icon).toBeInTheDocument() + expect(groupOption).toHaveAttribute('role', 'menuitemcheckbox') + }) + }) + + describe('defaultSelected', () => { + describe('if not provided', () => { + it('all group options has to be unselected', async () => { + render( + + + + + Option + + + Option + + + Option + + + + + ) + const options = screen.getAllByRole('menuitemcheckbox') + expect(options.length).toBe(3) + + options.forEach((option) => { + expect(option).toHaveAttribute('aria-checked', 'false') + }) + }) + + it('all checkboxes should not be visible', async () => { + const { container } = render( + + + + + Option + + + Option + + + Option + + + + + ) + const icons = container.querySelectorAll('svg[name="IconCheck"]') + + expect(icons.length).toBe(3) + + icons.forEach((icon) => { + expect(icon).not.toBeVisible() + }) + }) + }) + + describe('if provided', () => { + it('all selected checkboxes should be visible', async () => { + const selectedValues = ['item1', 'item3'] + const { container } = render( + + + + + Option + + + Option + + + Option + + + + + ) + const options = screen.getAllByRole('menuitemcheckbox') + const icons = container.querySelectorAll('svg') + + expect(options[0]).toHaveAttribute('aria-checked', 'true') + expect(options[1]).toHaveAttribute('aria-checked', 'false') + expect(options[2]).toHaveAttribute('aria-checked', 'true') + + expect(icons[0]).toBeVisible() + expect(icons[1]).not.toBeVisible() + expect(icons[2]).toBeVisible() + }) + + it("should be overridden by the Option's own defaultSelected", async () => { + const selectedValues = ['item1', 'item3'] + render( + + + + + Option + + + Option + + + Option + + + + + ) + const options = screen.getAllByRole('menuitemcheckbox') + + expect(options[0]).toHaveAttribute('aria-checked', 'true') // from the group, + expect(options[1]).toHaveAttribute('aria-checked', 'true') // from own prop, + expect(options[2]).toHaveAttribute('aria-checked', 'false') // override group's + }) + + describe('for "single" selectableType', () => { + it('the selected option should be checked', async () => { + const selectedValues = ['item2'] + render( + + + + + Option + + + Option + + + Option + + + + + ) + const options = screen.getAllByRole('menuitemradio') + + expect(options[0]).toHaveAttribute('aria-checked', 'false') + expect(options[1]).toHaveAttribute('aria-checked', 'true') + expect(options[2]).toHaveAttribute('aria-checked', 'false') + }) + + it("should throw error if multiple items are selected, and shouldn't select any item", async () => { + const selectedValues = ['item1', 'item3'] + render( + + + + + Option + + + Option + + + Option + + + + + ) + const options = screen.getAllByRole('menuitemradio') + + expect(options[0]).toHaveAttribute('aria-checked', 'false') + expect(options[1]).toHaveAttribute('aria-checked', 'false') + expect(options[2]).toHaveAttribute('aria-checked', 'false') + + expect(consoleErrorMock).toHaveBeenCalled() + }) + }) + }) + }) + + describe('selection', () => { + it('should fire "onSelect" callback', async () => { + const onSelect = vi.fn() + render( + + + + + Option 1 + + + Option 2 + + + Option 3 + + + + + ) + const option2 = screen.getByText('Option 2') + + await userEvent.click(option2) + + await waitFor(() => { + expect(onSelect).toHaveBeenCalledTimes(1) + + const args = onSelect.mock.calls[0][1] + const event = onSelect.mock.calls[0][0] + + expect(args).toHaveProperty('value', ['item2']) + expect(args).toHaveProperty('isSelected', true) + + expect(args.selectedOption).toBeInstanceOf(Object) + expect(args.selectedOption.props).toHaveProperty('id', 'groupOption02') + expect(args.selectedOption.props).toHaveProperty('value', 'item2') + + expect(args.drilldown).toBeInstanceOf(Object) + expect(args.drilldown.props).toHaveProperty('role', 'menu') + expect(args.drilldown.hide).toBeInstanceOf(Function) + + expect(event.target).toBe(option2) + }) + }) + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__tests__/DrilldownGroup.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__tests__/DrilldownGroup.test.tsx deleted file mode 100644 index c0152f1ba2..0000000000 --- a/packages/ui-drilldown/src/Drilldown/DrilldownGroup/__tests__/DrilldownGroup.test.tsx +++ /dev/null @@ -1,906 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import { expect, mount, find, stub, match } from '@instructure/ui-test-utils' - -import type { QueriesHelpersEventsType } from '@instructure/ui-test-queries' - -import { Drilldown } from '../../index' -import { DrilldownLocator } from '../../DrilldownLocator' - -const checkSelectedValues = async ( - options: QueriesHelpersEventsType[], - expectedSelectedValues: string[] -) => { - options.forEach((option) => { - const value = option.getAttribute('data-value')! - if (expectedSelectedValues.includes(value)) { - expect(option.getAttribute('aria-checked')).to.equal('true') - } else { - expect(option.getAttribute('aria-checked')).to.equal('false') - } - }) -} - -describe('', async () => { - it('should not allow de-selecting an option when selectableType = "single"', async () => { - const options = ['one', 'two', 'three'] - const Example = ({ opts }: { opts: typeof options }) => { - return ( - - - - {opts.map((opt) => { - return ( - - {opt} - - ) - })} - - - - ) - } - - await mount() - - let drilldown = await DrilldownLocator.find() - let selectedOption = await drilldown.find('#three') - - expect(selectedOption.getDOMNode().getAttribute('aria-checked')).to.be.eq( - 'true' - ) - - selectedOption.click() - - drilldown = await DrilldownLocator.find() - selectedOption = await drilldown.find('#three') - - expect(selectedOption.getDOMNode().getAttribute('aria-checked')).to.be.eq( - 'true' - ) - }) - - it("shouldn't render non-DrilldownGroup children", async () => { - stub(console, 'error') - await mount( - - - - Div - - - - ) - - const div = await find('#testDiv', { expectEmpty: true }) - - expect(div).to.not.exist() - }) - - it('elementRef should return ref to the group', async () => { - const elementRef = stub() - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const group = await drilldown.find('#group0') - - expect(elementRef).to.have.been.calledWith(group.getDOMNode()) - }) - - describe('renderGroupTitle', async () => { - it('should display', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const groupLabels = await drilldown.findAllGroupLabels() - - expect(groupLabels[0]).to.have.text('Group Title') - }) - - it('should display, if function is provided', async () => { - await mount( - - - 'Group Title'}> - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const groupLabels = await drilldown.findAllGroupLabels() - - expect(groupLabels[0]).to.have.text('Group Title') - }) - }) - - describe('separators', async () => { - it("shouldn't display, when group is first and/or last item", async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const separators = await drilldown.findAllSeparators({ - expectEmpty: true - }) - - expect(separators.length).to.equal(0) - }) - - it('should display by default, if group is between other items', async () => { - await mount( - - - Option - - Option - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const separators = await drilldown.findAllSeparators() - - expect(separators.length).to.equal(2) - }) - - it("shouldn't display extra separator between groups", async () => { - await mount( - - - Option - - Option - - - Option - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const separators = await drilldown.findAllSeparators() - - expect(separators.length).to.equal(3) - }) - }) - - describe('disabled prop', async () => { - it('should disable all items in the group', async () => { - await mount( - - - Option - - Option - Option - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - options.forEach((option) => { - const id = option.getAttribute('id')! - if (['groupOption01', 'groupOption02'].includes(id)) { - expect(option.getAttribute('aria-disabled')).to.equal('true') - } else { - expect(option.getAttribute('aria-disabled')).to.equal(null) - } - }) - }) - }) - - describe('role prop', async () => { - it('should be "group" by default', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const group = await drilldown.find('#group0') - - expect(group.getDOMNode()).to.have.attribute('role', 'group') - }) - - it('should render the group with passed role', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const group = await drilldown.find('#group0') - - expect(group.getDOMNode()).to.have.attribute('role', 'menu') - }) - }) - - describe('as prop', async () => { - it('should inherit Drilldown\'s "ul" by default', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const drilldownContainer = await drilldown.findSelectableContainer() - const group = await drilldown.find('#group0') - - expect(drilldownContainer.getDOMNode()).to.have.tagName('ul') - expect(group.getDOMNode()).to.have.tagName('ul') - }) - - it('should inherit Drilldown\'s "as" prop', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const drilldownContainer = await drilldown.findSelectableContainer() - const group = await drilldown.find('#group0') - - expect(drilldownContainer.getDOMNode()).to.have.tagName('ol') - expect(group.getDOMNode()).to.have.tagName('ol') - }) - - it('should render the group as other element', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const group = await drilldown.find('#group0') - - expect(group.getDOMNode()).to.have.tagName('ol') - }) - }) - - describe('selectableType', async () => { - it('if not set, should render role="menuitem" options without icon by default', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#groupOption01') - const optionWrapper = await drilldown.findOptionWrapperByOptionId( - 'groupOption01' - ) - - expect(option.getAttribute('role')).to.equal('menuitem') - expect( - await optionWrapper.find('[name="IconCheck"]', { - expectEmpty: true - }) - ).to.not.exist() - }) - - it('value "single" should render role="menuitemradio" options with icon', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#groupOption01') - const optionWrapper = await drilldown.findOptionWrapperByOptionId( - 'groupOption01' - ) - - expect(option.getAttribute('role')).to.equal('menuitemradio') - expect(await optionWrapper.find('[name="IconCheck"]')).to.exist() - }) - - it('value "multiple" should render role="menuitemcheckbox" options with icon', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#groupOption01') - const optionWrapper = await drilldown.findOptionWrapperByOptionId( - 'groupOption01' - ) - - expect(option.getAttribute('role')).to.equal('menuitemcheckbox') - expect(await optionWrapper.find('[name="IconCheck"]')).to.exist() - }) - }) - - describe('defaultSelected', async () => { - describe('if not provided', async () => { - it('all group options has to be unselected', async () => { - await mount( - - - - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await checkSelectedValues(options, []) - }) - - it('all checkboxes should not be visible', async () => { - await mount( - - - - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const [_groupOptionWrapper, ...optionWrappers] = - await drilldown.findAllOptionWrappers() - - for (const wrapper of optionWrappers) { - const icon = await wrapper.find('[name="IconCheck"]') - expect(getComputedStyle(icon.getDOMNode()).opacity).to.equal('0') - } - }) - }) - - describe('if provided', async () => { - it('all selected checkboxes should be visible', async () => { - const selectedValues = ['item1', 'item3'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const [_groupOptionWrapper, ...optionWrappers] = - await drilldown.findAllOptionWrappers() - - for (const wrapper of optionWrappers) { - const icon = await wrapper.find('[name="IconCheck"]') - const option = await wrapper.find('[data-value]') - const value = option.getAttribute('data-value')! - if (selectedValues.includes(value)) { - expect(getComputedStyle(icon.getDOMNode()).opacity).to.equal('1') - } else { - expect(getComputedStyle(icon.getDOMNode()).opacity).to.equal('0') - } - } - }) - - it("should be overridden by the Option's own defaultSelected", async () => { - const selectedValues = ['item1', 'item3'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - // first item from the group, - // second item from own prop, - // third item "false" should override group's - const expectedSelected = ['item1', 'item2'] - - await checkSelectedValues(options, expectedSelected) - }) - - describe('for "multiple" selectableType', async () => { - it('all selected options should be checked', async () => { - const selectedValues = ['item1', 'item3'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await checkSelectedValues(options, selectedValues) - }) - }) - - describe('for "single" selectableType', async () => { - it('the selected option should be checked', async () => { - const selectedValues = ['item2'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await checkSelectedValues(options, selectedValues) - }) - - it("should throw error if multiple items are selected, and shouldn't select any item", async () => { - const selectedValues = ['item1', 'item3'] - const consoleError = stub(console, 'error') - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - expect(consoleError).to.have.been.called() - - await checkSelectedValues(options, []) - }) - }) - }) - }) - - describe('selection', async () => { - it('should fire "onSelect" callback', async () => { - const onSelect = stub() - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const option2 = await drilldown.find('#groupOption02') - - await option2.click() - - expect(onSelect).to.have.been.calledWithMatch(match.object, { - value: ['item2'], - isSelected: true, - selectedOption: match.object, - drilldown: match.object - }) - - // 1st arg is the event - expect(onSelect.lastCall.args[0].target).to.equal(option2.getDOMNode()) - - expect(onSelect.lastCall.args[1].selectedOption.props.value).to.equal( - 'item2' - ) - expect(typeof onSelect.lastCall.args[1].drilldown.hide).to.equal( - 'function' - ) - }) - - describe('for "multiple" selectableType', async () => { - it('should toggle the selected option only', async () => { - const selectedValues = ['item1', 'item2', 'item3'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await checkSelectedValues(options, selectedValues) - - await options[1].click() - - await checkSelectedValues(options, ['item1', 'item3']) - }) - }) - - describe('for "single" selectableType', async () => { - it('should toggle options in radio fashion', async () => { - const selectedValues = ['item1'] - await mount( - - - - {/* data-value attr is for testing purposes only */} - - Option - - - Option - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await checkSelectedValues(options, ['item1']) - - await options[1].click() - - await checkSelectedValues(options, ['item2']) - }) - }) - }) - - describe('themeOverride prop', async () => { - it('should be passed to the Options component', async () => { - await mount( - - - - Option - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const groupLabels = await drilldown.findAllGroupLabels() - - const groupLabelStyle = getComputedStyle(groupLabels[0].getDOMNode()) - - expect(groupLabelStyle.color).to.equal('rgb(100, 0, 0)') - }) - }) -}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownLocator.ts b/packages/ui-drilldown/src/Drilldown/DrilldownLocator.ts deleted file mode 100644 index 801c72002a..0000000000 --- a/packages/ui-drilldown/src/Drilldown/DrilldownLocator.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import { locator } from '@instructure/ui-test-locator' -import { find, findAll, parseQueryArguments } from '@instructure/ui-test-utils' - -/* eslint-disable no-restricted-imports */ -// @ts-ignore: Cannot find module -import { OptionsItemLocator } from '@instructure/ui-options/es/Options/Item/OptionsItemLocator' -// @ts-ignore: Cannot find module -import { PopoverLocator } from '@instructure/ui-popover/es/Popover/PopoverLocator' -/* eslint-enable no-restricted-imports */ - -import { Drilldown } from './index' - -const customMethods = { - // return the wrapper Option.Items - findAllOptions: async (...args: any[]) => { - return await findAll( - '[class$=-optionItem__container]:not([role="presentation"])', - ...args - ) - }, - findAllOptionWrappers: async (...args: any[]) => { - return await OptionsItemLocator.findAll(...args) - }, - findOptionWrapperByOptionId: async ( - _element: any, - optionId: string, - ...args: any[] - ) => { - return OptionsItemLocator.find(`:has(#${optionId})`, ...args) - }, - findSizableContainer: async (...args: any[]) => { - return await find('[class$=-drilldown__container]', ...args) - }, - findSelectableContainer: async (...args: any[]) => { - return await find('[id^=Selectable_][id$=-list]', ...args) - }, - findHeaderTitle: async (...args: any[]) => { - return await find('[id^=DrilldownHeader-Title]', ...args) - }, - findHeaderActionOption: async (...args: any[]) => { - return await find('[id^=DrilldownHeader-Action]', ...args) - }, - findHeaderBackOption: async (...args: any[]) => { - return await find('[id^=DrilldownHeader-Back]', ...args) - }, - findHeaderSeparator: async (...args: any[]) => { - return await find('[id^=DrilldownHeader-Separator]', ...args) - }, - findAllSeparators: async (...args: any[]) => { - return await findAll('[class$=-separator]', ...args) - }, - findAllGroupLabels: async (...args: any[]) => { - return await findAll('[class$=-options__label]', ...args) - }, - findLabelInfo: async (...args: any[]) => { - return await find('[class$=-drilldown__optionLabelInfo]', ...args) - }, - findDescription: async (...args: any[]) => { - return await find('[class$=__description]', ...args) - }, - findPopoverRoot: (...args: any[]) => { - return PopoverLocator.find(...args) - }, - findPopoverTrigger: (...args: any[]) => { - return PopoverLocator.findTrigger(...args) - }, - findPopoverContent: (...args: any[]) => { - const { element, selector, options } = parseQueryArguments(...args) - return PopoverLocator.findContent(element, selector, { - ...options, - customMethods: { - ...options.customMethods, - ...customMethods - } - }) - } -} - -// @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message -export const DrilldownLocator = locator(Drilldown.selector, customMethods) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownOption/__new-tests__/DrilldownOption.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownOption/__new-tests__/DrilldownOption.test.tsx new file mode 100644 index 0000000000..5b60137ad6 --- /dev/null +++ b/packages/ui-drilldown/src/Drilldown/DrilldownOption/__new-tests__/DrilldownOption.test.tsx @@ -0,0 +1,774 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' + +import { IconCheckSolid } from '@instructure/ui-icons' + +import { Drilldown } from '../../index' + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + // Mocking console to prevent test output pollution and expect for messages + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it('should allow setting "selected" property on Options', async () => { + render( + + + + Option - 1 + + Option - 2 + + Option - 3 + Option - 4 + + + + ) + const selectedOption = screen.getByLabelText('Option - 2') + + expect(selectedOption).toBeInTheDocument() + expect(selectedOption).toHaveAttribute('id', 'groupOption02') + expect(selectedOption).toHaveAttribute('aria-checked', 'true') + }) + + describe('id prop', () => { + it('should throw warning the id is not provided', async () => { + render( + + + {/* @ts-expect-error: Testing behavior when `id` is missing */} + Option1 + + + ) + const expectedErrorMessage = + "Warning: Drilldown.Option without id won't be rendered. It is needed to internally track the options." + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + + it('should throw warning the id is duplicated', async () => { + render( + + + Option1 + Option2 + + + ) + const expectedErrorMessage = + 'Warning: Duplicate id: "option1"! Make sure all options have unique ids, otherwise they won\'t be rendered.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + + it('should not render the options with duplicated id', async () => { + render( + + + Option1 + Option2 + Option3 + + + ) + const menuitems = screen.queryAllByRole('menuitem') + const option = screen.queryByText('Option2') + + expect(menuitems.length).toBe(0) + expect(option).not.toBeInTheDocument() + }) + }) + + describe('children function prop', () => { + it('should throw warning if it returns nothing', async () => { + render( + + + {() => null} + + + ) + const expectedErrorMessage = + 'Warning: There are no "children" prop provided for option with id: "option1", so it won\'t be rendered.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + + it('should provide props as parameters', async () => { + const childrenFunction = vi.fn(() => 'Option') + + render( + + + {childrenFunction} + + + ) + + expect(childrenFunction).toHaveBeenCalledWith({ + id: 'option1', + variant: 'default', + isSelected: false + }) + }) + }) + + describe('elementRef prop', () => { + it('should give back to ref for the option wrapper (Options.Item)', async () => { + const elementRef = vi.fn() + const { container } = render( + + + + Option + + + + ) + const option = container.querySelector('li') + + expect(elementRef).toHaveBeenCalledWith(option) + }) + }) + + describe('subPageId prop', () => { + it('should display arrow icon', async () => { + const { container } = render( + + + + Option + + + + ) + const icon = container.querySelector('svg') + + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('name', 'IconArrowOpenEnd') + }) + + it('should indicate subpage fo SR', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + + expect(option).toHaveAttribute('role', 'button') + expect(option).toHaveAttribute('aria-haspopup', 'true') + }) + }) + + describe('disabled prop', () => { + it('should mark option as disabled for SR', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + + expect(option).toHaveAttribute('aria-disabled', 'true') + }) + }) + + describe('href prop', () => { + it('should display option as link', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + + expect(option.tagName).toBe('A') + expect(option).toHaveAttribute('href', '/helloWorld') + }) + + it('should throw warning if subPageId is provided too', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + const expectedErrorMessage = + 'Warning: Drilldown.Option with id "option1" has subPageId, so it will ignore the "href" property.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + + expect(option.tagName).not.toBe('A') + expect(option).not.toHaveAttribute('href') + }) + + it('should throw warning if option is in selectable group', async () => { + render( + + + + + Option + + + + + ) + const option = screen.getByLabelText('Option') + const expectedErrorMessage = + 'Warning: Drilldown.Option with id "groupOption01" is in a selectable group, so it will ignore the "href" property.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + + expect(option.tagName).not.toBe('A') + expect(option).not.toHaveAttribute('href') + }) + }) + + describe('as prop', () => { + it('should render option as `li` by default', async () => { + render( + + + Option + + + ) + const option = screen.getByLabelText('Option') + const wrapper = option.parentElement + + expect(option).toHaveAttribute('id', 'option1') + expect(wrapper?.tagName).toBe('LI') + }) + + it('should force option to be `li` while the parent is "ul" or "ol"', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + const wrapper = option.parentElement + + expect(option).toHaveAttribute('id', 'option1') + expect(wrapper?.tagName).toBe('LI') + }) + + it('should render option as specified html element, when the parent in non-list element', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + const wrapper = option.parentElement + + expect(option).toHaveAttribute('id', 'option1') + expect(wrapper?.tagName).toBe('DIV') + }) + }) + + describe('role prop', () => { + it('should be "menuitem" by default', async () => { + render( + + + Option + + + ) + const option = screen.getByLabelText('Option') + + expect(option).toHaveAttribute('role', 'menuitem') + }) + + it('should be applied on prop', async () => { + render( + + + + Option + + + + ) + const option = screen.getByLabelText('Option') + + expect(option).toHaveAttribute('role', 'presentation') + }) + }) + + describe('renderLabelInfo prop', () => { + it('should display tag next to the label', async () => { + const { container } = render( + + + + Option + + + + ) + const tag = container.querySelector('[class$="optionLabelInfo"]') + + expect(tag).toBeInTheDocument() + expect(tag).toHaveTextContent('Info') + }) + + it('as function should have option props as params', async () => { + const infoFunction = vi.fn(() => 'Info') + render( + + + + Option + + + + ) + + expect(infoFunction).toHaveBeenCalledWith({ + variant: 'default', + vAlign: 'start', + as: 'li', + role: 'menuitem', + isSelected: false + }) + }) + }) + + describe('renderBeforeLabel prop', () => { + it('should display icon before the label', async () => { + const { container } = render( + + + } + > + Option + + + + ) + const icon = container.querySelector('svg') + + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('name', 'IconCheck') + }) + + it('as function should have option props as params', async () => { + const beforeLabelFunction = vi.fn(() => ) + render( + + + + Option + + + + ) + + expect(beforeLabelFunction).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'default', + vAlign: 'start', + as: 'li', + role: 'menuitem', + isSelected: false + }), + expect.any(Object) + ) + }) + + it('should throw warning if it is in selectable group', async () => { + render( + + + + + Option + + + + + ) + const expectedErrorMessage = + 'Warning: The prop "renderBeforeLabel" is reserved on item with id: "groupOption1". When this option is a selectable member of a Drilldown.Group, selection indicator will render before the label.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + + describe('renderAfterLabel prop', () => { + it('should display icon before the label', async () => { + const { container } = render( + + + } + > + Option + + + + ) + const icon = container.querySelector('svg') + + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('name', 'IconCheck') + }) + + it('as function should have option props as params', async () => { + const beforeLabelFunction = vi.fn(() => ) + render( + + + + Option + + + + ) + + expect(beforeLabelFunction).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'default', + vAlign: 'start', + as: 'li', + role: 'menuitem', + isSelected: false + }), + expect.any(Object) + ) + }) + + it('should throw warning if it has subPageId', async () => { + render( + + + + Option + + + + ) + const expectedErrorMessage = + 'Warning: The prop "renderAfterLabel" is reserved on item with id: "option1". When it has "subPageId" provided, a navigation arrow will render after the label.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + + describe('description prop', () => { + it('should display description under the option', async () => { + render( + + + + Option + + + + ) + const description = screen.getByText('This is a description.') + + expect(description).toBeInTheDocument() + }) + + it('as a function should display description under the option', async () => { + render( + + + 'This is a description.'} + > + Option + + + + ) + const description = screen.getByText('This is a description.') + + expect(description).toBeInTheDocument() + }) + }) + + describe('descriptionRole prop', () => { + it('should set the role of description', async () => { + render( + + + + Option + + + + ) + const description = screen.getByText('This is a description.') + + expect(description).toHaveAttribute('role', 'button') + }) + }) + + describe('onOptionClick callback', () => { + it('should fire on click with correct params', async () => { + const onOptionClick = vi.fn() + render( + + + + Option + + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + expect(onOptionClick).toHaveBeenCalledTimes(1) + + const args = onOptionClick.mock.calls[0][1] + + expect(args).toHaveProperty('optionId', 'option1') + expect(args).toHaveProperty('pageHistory', ['page0']) + + expect(args.drilldown).toBeInstanceOf(Object) + expect(args.drilldown.props).toHaveProperty('role', 'menu') + + expect(args.goToPage).toBeInstanceOf(Function) + expect(args.goToPreviousPage).toBeInstanceOf(Function) + }) + }) + + it('should provide goToPreviousPage method that throws a warning, if there is no previous page', async () => { + render( + + + { + goToPreviousPage() + }} + > + Option + + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const expectedErrorMessage = + 'Warning: There is no previous page to go to. The current page history is: [page0].' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + + describe('provide goToPage method', () => { + it("should throws warning if page doesn't exist", async () => { + render( + + + { + goToPage('page1') + }} + > + Option + + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const expectedErrorMessage = + 'Warning: Cannot go to page because page with id: "page1" doesn\'t exist.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + + it('should throws warning if if no page id is provided', async () => { + render( + + + { + // @ts-expect-error we want this to fail + goToPage() + }} + > + Option + + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const expectedErrorMessage = + 'Warning: Cannot go to page because there was no page id provided.' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + + it('should throws warning if parameter is not string', async () => { + render( + + + { + // @ts-expect-error we want this to fail + goToPage({ page: 'page1' }) + }} + > + Option + + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const expectedErrorMessage = + 'Warning: Cannot go to page because parameter newPageId has to be string (valid page id). Current newPageId is "object".' + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining(expectedErrorMessage), + expect.any(String) + ) + }) + }) + }) + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownOption/__tests__/DrilldownOption.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownOption/__tests__/DrilldownOption.test.tsx deleted file mode 100644 index 26ad74486b..0000000000 --- a/packages/ui-drilldown/src/Drilldown/DrilldownOption/__tests__/DrilldownOption.test.tsx +++ /dev/null @@ -1,1071 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import { expect, match, mount, stub } from '@instructure/ui-test-utils' - -import { IconCheckSolid } from '@instructure/ui-icons' - -import { Drilldown } from '../../index' -import { DrilldownLocator } from '../../DrilldownLocator' - -describe('', async () => { - it('should allow setting "selected" property on Options', async () => { - await mount( - - - - Option - 1 - - Option - 2 - - Option - 3 - Option - 4 - - - - ) - const drilldown = await DrilldownLocator.find() - const selectedOption = await drilldown.find('#groupOption02') - - expect(selectedOption.getDOMNode().getAttribute('aria-checked')).to.be.eq( - 'true' - ) - }) - it('should allow controlled behaviour', async () => { - const options = ['one', 'two', 'three'] - const Example = ({ - opts, - selected - }: { - opts: typeof options - selected: string - }) => { - return ( - - - - {opts.map((opt) => { - return ( - - {opt} - - ) - })} - - - - ) - } - const subject = await mount() - let drilldown = await DrilldownLocator.find() - let opts = await drilldown.findAllOptions() - - expect(opts[0].getDOMNode().getAttribute('aria-checked')).to.be.eq('false') - expect(opts[1].getDOMNode().getAttribute('aria-checked')).to.be.eq('true') - expect(opts[2].getDOMNode().getAttribute('aria-checked')).to.be.eq('false') - - subject.setProps({ selected: 'three' }) - - drilldown = await DrilldownLocator.find() - opts = await drilldown.findAllOptions() - - expect(opts[0].getDOMNode().getAttribute('aria-checked')).to.be.eq('false') - expect(opts[1].getDOMNode().getAttribute('aria-checked')).to.be.eq('false') - expect(opts[2].getDOMNode().getAttribute('aria-checked')).to.be.eq('true') - }) - describe('id prop', async () => { - it('should throw warning the id is not provided', async () => { - stub(console, 'error') - const consoleWarning = stub(console, 'warn') - - await mount( - - - {/* @ts-expect-error we want this to fail*/} - Option1 - - - ) - - expect(consoleWarning).to.have.been.calledWith( - "Warning: Drilldown.Option without id won't be rendered. It is needed to internally track the options." - ) - }) - - it('should throw warning the id is duplicated', async () => { - const consoleWarning = stub(console, 'warn') - - await mount( - - - Option1 - Option2 - - - ) - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Duplicate id: "option1"! Make sure all options have unique ids, otherwise they won\'t be rendered.' - ) - }) - - it('should not render the options with duplicated id', async () => { - stub(console, 'warn') - - await mount( - - - Option1 - Option2 - Option3 - - - ) - - const drilldown = await DrilldownLocator.find() - const allOptions = await drilldown.findAllOptions({ expectEmpty: true }) - - expect(allOptions.length).to.equal(0) - }) - }) - - describe('children function prop', async () => { - it('should throw warning if it returns nothing', async () => { - const consoleWarning = stub(console, 'warn') - - await mount( - - - {() => null} - - - ) - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: There are no "children" prop provided for option with id: "option1", so it won\'t be rendered.' - ) - }) - - it('should provide props as parameters', async () => { - const childrenFunction = stub() - childrenFunction.returns('Option') - - await mount( - - - {childrenFunction} - - - ) - - expect(childrenFunction).to.have.been.calledWith({ - id: 'option1', - variant: 'default', - isSelected: false - }) - }) - }) - - describe('elementRef prop', async () => { - it('should give back to ref for the option wrapper (Options.Item)', async () => { - const elementRef = stub(console, 'warn') - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - - expect(elementRef).to.have.been.calledWith(wrapper.getDOMNode()) - }) - }) - - describe('subPageId prop', async () => { - it('should display arrow icon', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - const icon = await wrapper.find('[name="IconArrowOpenEnd"]') - - expect(icon).to.exist() - }) - - it('should indicate subpage fo SR', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getAttribute('aria-haspopup')).to.equal('true') - expect(option.getAttribute('role')).to.equal('button') - }) - - it('should navigate to subPage on select', async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option01') - - await option.click() - - const option2 = await drilldown.find('#option11') - - expect(option2).to.be.visible() - }) - }) - - describe('disabled prop', async () => { - it('should mark option as disabled for SR', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getAttribute('aria-disabled')).to.equal('true') - }) - - it('should apply disabled css style', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - const style = getComputedStyle(option.getDOMNode()) - - expect(style.color).to.equal('rgb(39, 53, 64)') - expect(style.cursor).to.equal('not-allowed') - }) - }) - - describe('href prop', async () => { - it('should display option as link', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getTagName()).to.equal('a') - expect(option.getAttribute('href')).to.equal('/helloWorld') - }) - - it('should navigate to url on Focus + Space', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - await option.focus() - await option.keyDown('space') - - expect(window.location.hash).to.equal('#helloWorld') - // reset hash - window.location.hash = '' - }) - - it('should navigate to url on Click', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - await option.click() - - expect(window.location.hash).to.equal('#helloWorld') - // reset hash - window.location.hash = '' - }) - - it("shouldn't navigate to url, if disabled", async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - // reset hash - window.location.hash = '' - - await option.click() - - expect(window.location.hash).to.equal('') - }) - - it('should throw warning if subPageId is provided too', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getTagName()).to.not.equal('a') - expect(option.getAttribute('href')).to.equal(null) - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Drilldown.Option with id "option1" has subPageId, so it will ignore the "href" property.' - ) - }) - - it('should throw warning if option is in selectable group', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - - - Option - - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#groupOption01') - - expect(option.getTagName()).to.not.equal('a') - expect(option.getAttribute('href')).to.equal(null) - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Drilldown.Option with id "groupOption01" is in a selectable group, so it will ignore the "href" property.' - ) - }) - }) - - describe('as prop', async () => { - it('should render option as `li` by default', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - - expect(wrapper.getDOMNode()).to.have.tagName('li') - }) - - it('should force option to be `li` while the parent is "ul" or "ol"', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - - expect(wrapper.getDOMNode()).to.have.tagName('li') - }) - - it('should render option as specified html element, when the parent in non-list element', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - - expect(wrapper.getDOMNode()).to.have.tagName('div') - }) - }) - - describe('role prop', async () => { - it('should be "menuitem" by default', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getAttribute('role')).to.equal('menuitem') - }) - - it('should be applied on prop', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - expect(option.getAttribute('role')).to.equal('presentation') - }) - }) - - describe('renderLabelInfo prop', async () => { - it('should display tag next to the label', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const tag = await drilldown.findLabelInfo() - - expect(tag).to.exist() - }) - - it('as function should have option props as params', async () => { - const infoFunction = stub() - infoFunction.returns('Info') - - await mount( - - - - Option - - - - ) - - expect(infoFunction).to.have.been.calledWith({ - variant: 'default', - vAlign: 'start', - as: 'li', - role: 'menuitem', - isSelected: false - }) - }) - - it('should be affected by afterLabelContentVAlign prop', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const tag = await drilldown.findLabelInfo() - - expect(getComputedStyle(tag.getDOMNode()).alignSelf).to.equal('flex-end') - }) - }) - - describe('renderBeforeLabel prop', async () => { - it('should display icon before the label', async () => { - await mount( - - - } - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - const icon = await wrapper.find('[name="IconCheck"]') - - expect(icon).to.exist() - }) - - it('as function should have option props as params', async () => { - const beforeLabelFunction = stub() - beforeLabelFunction.returns() - - await mount( - - - - Option - - - - ) - - expect(beforeLabelFunction).to.have.been.calledWith({ - variant: 'default', - vAlign: 'start', - as: 'li', - role: 'menuitem', - isSelected: false - }) - }) - - it('should throw warning if it is in selectable group', async () => { - const consoleWarning = stub(console, 'warn') - - await mount( - - - - - Option - - - - - ) - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: The prop "renderBeforeLabel" is reserved on item with id: "groupOption1". When this option is a selectable member of a Drilldown.Group, selection indicator will render before the label.' - ) - }) - }) - - describe('renderAfterLabel prop', async () => { - it('should display icon before the label', async () => { - await mount( - - - } - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const wrapper = await drilldown.findOptionWrapperByOptionId('option1') - const icon = await wrapper.find('[name="IconCheck"]') - - expect(icon).to.exist() - }) - - it('as function should have option props as params', async () => { - const beforeLabelFunction = stub() - beforeLabelFunction.returns() - - await mount( - - - - Option - - - - ) - - expect(beforeLabelFunction).to.have.been.calledWith({ - variant: 'default', - vAlign: 'start', - as: 'li', - role: 'menuitem', - isSelected: false - }) - }) - - it('should throw warning if it has subPageId', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - - Option - - - - ) - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: The prop "renderAfterLabel" is reserved on item with id: "option1". When it has "subPageId" provided, a navigation arrow will render after the label.' - ) - }) - }) - - describe('description prop', async () => { - it('should display description under the option', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const description = await drilldown.findDescription() - - expect(description).to.have.text('This is a description.') - }) - - it('as a function should display description under the option', async () => { - await mount( - - - 'This is a description.'} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const description = await drilldown.findDescription() - - expect(description).to.have.text('This is a description.') - }) - }) - - describe('descriptionRole prop', async () => { - it('should set the role of description', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const description = await drilldown.findDescription() - - expect(description.getAttribute('role')).to.equal('button') - }) - }) - - describe('onOptionClick callback', async () => { - it('should fire on click with correct params', async () => { - const onOptionClick = stub() - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const option = await drilldown.find('#option1') - - await option.click() - - expect(onOptionClick).to.have.been.calledWithMatch(match.object, { - optionId: 'option1', - drilldown: match.object, - pageHistory: ['page0'], - goToPage: match.func, - goToPreviousPage: match.func - }) - }) - - describe('should provide goToPreviousPage method', async () => { - it('that goes back to the previous page', async () => { - await mount( - - - - Option - - - - - { - goToPreviousPage() - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - const page1Option = await drilldown.find('#option11') - - await page1Option.click() - - const page0Option2 = await drilldown.find('#option01') - - expect(page0Option2).to.be.visible() - }) - - it('that throws a warning, if there is no previous page', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - { - goToPreviousPage() - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: There is no previous page to go to. The current page history is: [page0].' - ) - }) - }) - - describe('should provide goToPage method', async () => { - it('that can be used to go back a page', async () => { - await mount( - - - - Option - - - - - { - goToPage(pageHistory[0]) - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - const page1Option = await drilldown.find('#option11') - - await page1Option.click() - - const page0Option2 = await drilldown.find('#option01') - - expect(page0Option2).to.be.visible() - }) - - it('that can be used to go to a new, existing page', async () => { - await mount( - - - { - goToPage('page1') - }} - > - Option - - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - const page1Option = await drilldown.find('#option11') - - expect(page1Option).to.be.visible() - }) - - describe('that throws warning', async () => { - it("if page doesn't exist", async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - { - goToPage('page1') - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Cannot go to page because page with id: "page1" doesn\'t exist.' - ) - }) - - it('if if no page id is provided', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - { - // @ts-expect-error we want this to fail - goToPage() - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Cannot go to page because there was no page id provided.' - ) - }) - - it('if parameter is not string', async () => { - const consoleWarning = stub(console, 'warn') - await mount( - - - { - // @ts-expect-error we want this to fail - goToPage({ page: 'page1' }) - }} - > - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01') - - await page0Option.click() - - expect(consoleWarning).to.have.been.calledWith( - 'Warning: Cannot go to page because parameter newPageId has to be string (valid page id). Current newPageId is "object".' - ) - }) - }) - }) - }) - - describe('themeOverride prop', async () => { - it('should be passed to the Options.Item component', async () => { - await mount( - - - - Option - - - - ) - - const drilldown = await DrilldownLocator.find() - const optionWrapper = await drilldown.findOptionWrapperByOptionId( - 'option01' - ) - const groupLabelStyle = getComputedStyle(optionWrapper.getDOMNode()) - - expect(groupLabelStyle.color).to.equal('rgb(0, 0, 100)') - expect(groupLabelStyle.backgroundColor).to.equal('rgb(200, 200, 200)') - }) - }) -}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownPage/__new-tests__/DrilldownPage.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownPage/__new-tests__/DrilldownPage.test.tsx new file mode 100644 index 0000000000..b6ad0fc210 --- /dev/null +++ b/packages/ui-drilldown/src/Drilldown/DrilldownPage/__new-tests__/DrilldownPage.test.tsx @@ -0,0 +1,352 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' + +import { Drilldown } from '../../index' + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + // Mocking console to prevent test output pollution and expect for messages + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it("shouldn't render non-DrilldownPage children", async () => { + render( + + + Div + + + ) + const nonPageChild = screen.queryByText('DIV') + + expect(nonPageChild).not.toBeInTheDocument() + }) + + describe('header title', () => { + it('should be displayed in the header', async () => { + const { container } = render( + + + Option + + + ) + const titleContainer = container.querySelector( + '[id^="DrilldownHeader-Title-Label_"]' + ) + const title = screen.queryByText('HeaderTitleString') + + expect(titleContainer).toBeInTheDocument() + expect(title).toBeInTheDocument() + }) + + it('should be displayed when function is passed', async () => { + render( + + 'HeaderTitleFunction'}> + Option + + + ) + const title = screen.queryByText('HeaderTitleFunction') + + expect(title).toBeInTheDocument() + }) + + it("shouldn't be displayed when function has no return value", async () => { + const { container } = render( + + null}> + Option + + + ) + const title = container.querySelector( + '[id^="DrilldownHeader-Title-Label_"]' + ) + const listItem = container.querySelector('li') + + expect(title).not.toBeInTheDocument() + expect(listItem).toBeInTheDocument() + }) + }) + + describe('header action label', () => { + it('should be displayed in the header', async () => { + render( + + + Option + + + ) + const actionLabel = screen.queryByText('HeaderActionLabelString') + + expect(actionLabel).toBeInTheDocument() + }) + + it('should be displayed when function is passed', async () => { + render( + + 'HeaderActionLabelFunction'} + > + Option + + + ) + const actionLabel = screen.queryByText('HeaderActionLabelFunction') + + expect(actionLabel).toBeInTheDocument() + }) + + it("shouldn't be displayed when function has no return value", async () => { + const { container } = render( + + null}> + Option + + + ) + const listItem = container.querySelector('li') + const title = container.querySelector( + '[id^="DrilldownHeader-Title-Label_"]' + ) + + expect(listItem).toBeInTheDocument() + expect(title).not.toBeInTheDocument() + }) + + it('should fire header action callback', async () => { + const actionCallback = vi.fn() + render( + + + Option + + + ) + const actionLabel = screen.getByText('ActionWithCallback') + + await userEvent.click(actionLabel) + + await waitFor(() => { + expect(actionCallback).toHaveBeenCalled() + }) + }) + }) + + describe('header back navigation', () => { + it('should not be displayed on root page', async () => { + render( + + + Option + + + ) + const label = screen.queryByText('HeaderBackString') + + expect(label).not.toBeInTheDocument() + }) + + it('should be displayed on subpages', async () => { + render( + + + + Option + + + + Option + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const label = screen.queryByText('HeaderBackString') + + expect(label).toBeInTheDocument() + }) + }) + + it('should be displayed when function is passed, and can use former page title', async () => { + const pageTitle = 'Page Title' + render( + + + + Option + + + + `Back to ${prevPageTitle}` + } + > + Option + + + ) + const option = screen.getByText('Option') + + await userEvent.click(option) + + await waitFor(() => { + const label = screen.queryByText(`Back to ${pageTitle}`) + + expect(label).toBeInTheDocument() + }) + }) + }) + + describe('header separator', () => { + it('should not be displayed, if no item is on the header', async () => { + const { container } = render( + + + Option + + + ) + const separator = container.querySelector( + '[id^="DrilldownHeader-Separator_"]' + ) + + expect(separator).not.toBeInTheDocument() + }) + + it('should be displayed, if there are items in the header', async () => { + const { container } = render( + + + Option + + + ) + const separator = container.querySelector( + '[id^="DrilldownHeader-Separator_"]' + ) + + expect(separator).toBeInTheDocument() + }) + + it('should not be displayed, if withoutHeaderSeparator is set', async () => { + const { container } = render( + + + Option + + + ) + const separator = container.querySelector( + '[id^="DrilldownHeader-Separator_"]' + ) + + expect(separator).not.toBeInTheDocument() + }) + }) + + describe('disabled prop', () => { + it('should make all options disabled', async () => { + render( + + + Option + + + ) + const allOptions = screen.getAllByRole('menuitem') + + allOptions.forEach((option) => { + expect(option).toHaveAttribute('aria-disabled', 'true') + }) + }) + + it("shouldn't make header Back options disabled", async () => { + render( + + + + Option1 + + + + Option2 + + + ) + const subPageOption = screen.getByText('Option1') + + await userEvent.click(subPageOption) + + await waitFor(() => { + const option2 = screen.getByLabelText('Option2') + const backOption = screen.getByLabelText('HeaderBackString') + + expect(option2).toHaveAttribute('role', 'menuitem') + expect(option2).toHaveAttribute('aria-disabled', 'true') + + expect(backOption).toHaveAttribute('role', 'menuitem') + expect(backOption).not.toHaveAttribute('aria-disabled') + }) + }) + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownPage/__tests__/DrilldownPage.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownPage/__tests__/DrilldownPage.test.tsx deleted file mode 100644 index db2b1ae8c3..0000000000 --- a/packages/ui-drilldown/src/Drilldown/DrilldownPage/__tests__/DrilldownPage.test.tsx +++ /dev/null @@ -1,475 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import { expect, mount, find, stub, wait } from '@instructure/ui-test-utils' - -import { Drilldown } from '../../index' -import { DrilldownLocator } from '../../DrilldownLocator' - -describe('', async () => { - it("shouldn't render non-DrilldownPage children", async () => { - stub(console, 'error') - await mount( - - - Div - - - ) - - const div = await find('#testDiv', { expectEmpty: true }) - - expect(div).to.not.exist() - }) - - describe('header title', async () => { - it('should be displayed in the header', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const headerTitleOption = await drilldown.findHeaderTitle() - - expect( - await headerTitleOption.find(':contains(HeaderTitleString)') - ).to.exist() - }) - - it('should be displayed when function is passed', async () => { - await mount( - - 'HeaderTitleFunction'}> - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const headerTitleOption = await drilldown.findHeaderTitle() - - expect( - await headerTitleOption.find(':contains(HeaderTitleFunction)') - ).to.exist() - }) - - it("shouldn't be displayed when function has no return value", async () => { - await mount( - - null}> - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const allOptions = await drilldown.findAllOptions() - - // it should only contain the 1 item but not the header - expect(allOptions.length).to.equal(1) - }) - }) - - describe('header action label', async () => { - it('should be displayed in the header', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const headerActionOption = await drilldown.findHeaderActionOption() - - expect( - await headerActionOption.find(':contains(HeaderActionLabelString)') - ).to.exist() - }) - - it('should be displayed when function is passed', async () => { - await mount( - - 'HeaderActionLabelFunction'} - > - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const headerActionOption = await drilldown.findHeaderActionOption() - - expect( - await headerActionOption.find(':contains(HeaderActionLabelFunction)') - ).to.exist() - }) - - it("shouldn't be displayed when function has no return value", async () => { - await mount( - - null}> - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const allOptions = await drilldown.findAllOptions() - - // it should only contain the 1 item but not the header - expect(allOptions.length).to.equal(1) - }) - - it('should fire header action callback', async () => { - const actionCallback = stub() - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const actionOption = await drilldown.findHeaderActionOption() - - await actionOption.click() - - await wait(() => { - expect(actionCallback).to.have.been.called() - }) - }) - }) - - describe('header back navigation', async () => { - it('should not be displayed on root page', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const headerBackOption = await drilldown.findHeaderBackOption({ - expectEmpty: true - }) - - expect(headerBackOption).to.not.exist() - }) - - it('should be displayed on subpages', async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - - expect( - await headerBackOption.find(':contains(HeaderBackString)') - ).to.exist() - }) - - it('should be displayed when function is passed, and can use former page title', async () => { - await mount( - - - - Option - - - - `Back to ${prevPageTitle}` - } - > - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - - expect( - await headerBackOption.find(':contains(Back to Page Title)') - ).to.exist() - }) - - it('should have a back arrow', async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - const headerBackOptionContainer = await headerBackOption.getParentNode()! - - expect( - await headerBackOptionContainer.querySelector( - 'svg[name="IconArrowOpenStart"]' - ) - ).to.exist() - }) - - it('should still display the back icon, even if function has no return value', async () => { - await mount( - - - - Option - - - null}> - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - const headerBackOptionContainer = await headerBackOption.getParentNode()! - - expect( - await headerBackOptionContainer.querySelector( - 'svg[name="IconArrowOpenStart"]' - ) - ).to.exist() - }) - - it('should fire onBackButtonClicked on click', async () => { - const backNavCallback = stub() - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - - await headerBackOption.click() - - await wait(() => { - expect(backNavCallback).to.have.been.called() - }) - }) - - it('should go back one page on click', async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const headerBackOption = await drilldown.findHeaderBackOption() - - await headerBackOption.click() - - const subPageOption2 = await drilldown.find('#option1') - - expect(subPageOption2).to.exist() - }) - }) - - describe('header separator', async () => { - it('should not be displayed, if no item is on the header', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const separator = await drilldown.findHeaderSeparator({ - expectEmpty: true - }) - - expect(separator).to.not.exist() - }) - - it('should be displayed, if there are items in the header', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const separator = await drilldown.findHeaderSeparator() - - expect(separator).to.exist() - }) - - it('should be displayed, if withoutHeaderSeparator is set', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const separator = await drilldown.findHeaderSeparator({ - expectEmpty: true - }) - - expect(separator).to.not.exist() - }) - }) - - describe('disabled prop', async () => { - it('should make all options disabled', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const allOptions = await drilldown.findAllOptions() - - allOptions.forEach((option) => { - expect(option.getAttribute('aria-disabled')).to.equal('true') - }) - }) - - it("shouldn't make header Back options disabled", async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const subPageOption = await drilldown.find('#option1') - - await subPageOption.click() - - const allOptions = await drilldown.findAllOptions() - - allOptions.forEach((option) => { - if (option.getAttribute('id')!.includes('DrilldownHeader-Back')) { - expect(option.getAttribute('aria-disabled')).to.equal(null) - } else { - expect(option.getAttribute('aria-disabled')).to.equal('true') - } - }) - }) - }) -}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__new-tests__/DrilldownSeparator.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__new-tests__/DrilldownSeparator.test.tsx new file mode 100644 index 0000000000..d5850bcf73 --- /dev/null +++ b/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__new-tests__/DrilldownSeparator.test.tsx @@ -0,0 +1,60 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { Drilldown } from '../../index' + +describe('', () => { + it('should render', async () => { + render( + + + + + + ) + const separator = screen.getByTestId('separator1') + + expect(separator).toBeVisible() + expect(separator.getAttribute('class')).toContain('-separator') + }) + + it('should not render children', async () => { + render( + + + + Children + + + + ) + const separatorChild = screen.queryByText('Children') + + expect(separatorChild).not.toBeInTheDocument() + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__tests__/DrilldownSeparator.test.tsx b/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__tests__/DrilldownSeparator.test.tsx deleted file mode 100644 index 9ff9f1f96a..0000000000 --- a/packages/ui-drilldown/src/Drilldown/DrilldownSeparator/__tests__/DrilldownSeparator.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import { expect, mount, find } from '@instructure/ui-test-utils' - -import { Drilldown } from '../../index' -import { DrilldownLocator } from '../../DrilldownLocator' - -describe('', async () => { - it('should render', async () => { - await mount( - - - - - - ) - - const separator = await find('#separator1') - - expect(separator).to.be.visible() - }) - - it('should not render children', async () => { - await mount( - - - Children - - - ) - - const separator = await find('#separator1') - - expect(separator.getTextContent()).to.equal('') - }) - - it('as prop should apply', async () => { - await mount( - - - - - - ) - - const drilldown = await DrilldownLocator.find() - const separator = await drilldown.find('#separator1') - const separatorWrapper = separator.getDOMNode().parentNode as HTMLElement - - expect(separatorWrapper.tagName.toLowerCase()).to.equal('li') - }) - - it('themeOverride prop should pass overrides to Option.Separator', async () => { - await mount( - - - - - - ) - - const drilldown = await DrilldownLocator.find() - const separator = await drilldown.find('#separator1') - const separatorStyle = getComputedStyle(separator.getDOMNode()) - - expect(separatorStyle.height).to.equal('16px') - expect(separatorStyle.backgroundColor).to.equal('rgb(0, 128, 0)') - }) -}) diff --git a/packages/ui-drilldown/src/Drilldown/__new-tests__/Drilldown.test.tsx b/packages/ui-drilldown/src/Drilldown/__new-tests__/Drilldown.test.tsx new file mode 100644 index 0000000000..af07b0f26b --- /dev/null +++ b/packages/ui-drilldown/src/Drilldown/__new-tests__/Drilldown.test.tsx @@ -0,0 +1,979 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' + +// eslint-disable-next-line no-restricted-imports +import { generateA11yTests } from '@instructure/ui-scripts/lib/test/generateA11yTests' +import { runAxeCheck } from '@instructure/ui-axe-check' +import { IconCheckSolid } from '@instructure/ui-icons' +import { Popover } from '@instructure/ui-popover' + +import { Drilldown } from '../index' +import DrilldownExamples from '../__examples__/Drilldown.examples' + +const data = Array(5) + .fill(0) + .map((_v, ind) => ({ + label: `option ${ind}`, + id: `opt_${ind}` + })) + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + // Mocking console to prevent test output pollution and expect for messages + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + describe('rootPageId prop', () => { + it('should set the initial page and render it', async () => { + render( + + + Option-01 + + + Option-11 + + + ) + + const drilldown = screen.getByRole('menu') + const options = screen.getAllByRole('menuitem') + const rootlessOption = screen.queryByText('Option-01') + + expect(drilldown).toBeInTheDocument() + expect(rootlessOption).not.toBeInTheDocument() + + expect(options.length).toBe(1) + expect(options[0]).toHaveTextContent('Option-11') + expect(options[0]).toHaveAttribute('id', 'option11') + }) + }) + + describe('children prop', () => { + it('should not allow non-DrilldownPage children', async () => { + render( + + DIV-child + + ) + + const drilldown = screen.queryByRole('menu') + const options = screen.queryAllByRole('menuitem') + const notAllowedChild = screen.queryByText('DIV-child') + + expect(drilldown).not.toBeInTheDocument() + expect(options.length).toBe(0) + expect(notAllowedChild).not.toBeInTheDocument() + }) + }) + + describe('id prop', () => { + it('should put id attr on the drilldown', async () => { + render( + + + Option + + + ) + + const drilldown = screen.getByRole('menu') + + expect(drilldown).toBeInTheDocument() + expect(drilldown).toHaveAttribute('id', 'testId') + }) + }) + + describe('label prop', () => { + it('should be added as aria-label', async () => { + render( + + + Option + + + ) + + const drilldown = screen.getByRole('menu') + + expect(drilldown).toBeInTheDocument() + expect(drilldown).toHaveAttribute('aria-label', 'testLabel') + }) + }) + + describe('disabled prop', () => { + it('should disable all options', async () => { + render( + + + Option + Option + Option + + + ) + const options = screen.getAllByRole('menuitem') + + expect(options.length).toBe(4) // header action + 3 options + + options.forEach((option) => { + expect(option).toHaveAttribute('aria-disabled', 'true') + }) + }) + }) + + describe('as prop', () => { + it('should be "ul" by default', async () => { + const { container } = render( + + + Option + + + ) + const drilldownList = container.querySelector('[class$="-options__list"]') + + expect(drilldownList?.tagName).toBe('UL') + }) + + it('should render as passed element', async () => { + const { container } = render( + + + Option + + + ) + const drilldownList = container.querySelector('[class$="-options__list"]') + + expect(drilldownList?.tagName).toBe('OL') + }) + }) + + describe('role prop', () => { + it('should be "menu" by default', async () => { + const { container } = render( + + + Option + + + ) + const drilldown = container.querySelector('div[id^="Drilldown_"]') + + expect(drilldown).toHaveAttribute('role', 'menu') + }) + + it('should apply passed role', async () => { + const { container } = render( + + + Option + + + ) + const drilldown = container.querySelector('div[id^="Drilldown_"]') + + expect(drilldown).toHaveAttribute('role', 'list') + }) + }) + + describe('elementRef prop (and ref static prop)', () => { + it('should give back the drilldown element when there is no trigger', async () => { + const elementRef = vi.fn() + + render( + + + Option + + + ) + const drilldown = screen.getByRole('menu') + + expect(drilldown).toBeInTheDocument() + expect(elementRef).toHaveBeenCalledWith(drilldown) + }) + + it('should give back the Popover root when drilldown has trigger and is closed', async () => { + const elementRef = vi.fn() + const { container } = render( + Toggle} + > + + Option + + + ) + const trigger = screen.getByText('Toggle') + const positionId = trigger.getAttribute('data-position-target') + const drilldownRoot = container.querySelector( + `span[data-position="${positionId}"]` + ) + + expect(drilldownRoot).toBeInTheDocument() + expect(elementRef).toHaveBeenCalledWith(drilldownRoot) + }) + + it('should give back the the Popover root when drilldown has trigger and is open', async () => { + const elementRef = vi.fn() + const { container } = render( + Toggle} + defaultShow + > + + Option + + + ) + const trigger = screen.getByText('Toggle') + const positionId = trigger.getAttribute('data-position-target') + const drilldownRoot = container.querySelector( + `span[data-position="${positionId}"]` + ) + + expect(drilldownRoot).toBeInTheDocument() + expect(elementRef).toHaveBeenCalledWith(drilldownRoot) + }) + }) + + describe('drilldownRef prop', () => { + it('should give back the drilldown element when there is no trigger', async () => { + const drilldownRef = vi.fn() + render( + + + Option + + + ) + const drilldown = screen.getByRole('menu') + + expect(drilldownRef).toHaveBeenCalledWith(drilldown) + }) + + it("shouldn't be called when drilldown has trigger and is closed", async () => { + const drilldownRef = vi.fn() + render( + Toggle} + > + + Option + + + ) + + expect(drilldownRef).not.toHaveBeenCalled() + }) + + it('should give back the drilldown element when drilldown has trigger and is open', async () => { + const drilldownRef = vi.fn() + render( + Toggle} + defaultShow + > + + Option + + + ) + const drilldown = screen.getByRole('menu') + + expect(drilldownRef).toHaveBeenCalledWith(drilldown) + }) + }) + + describe('popoverRef prop', () => { + it('should not be called when there is no trigger', async () => { + const popoverRef = vi.fn() + render( + + + Option + + + ) + + expect(popoverRef).not.toHaveBeenCalled() + }) + + it('should give back the Popover component when drilldown has trigger and is closed', async () => { + const popoverRef = vi.fn() + const { container } = render( + Toggle} + > + + Option + + + ) + const trigger = screen.getByText('Toggle') + const positionId = trigger.getAttribute('data-position-target') + const popoverRoot = container.querySelector( + `span[data-position="${positionId}"]` + ) + + expect(popoverRoot).toBeInTheDocument() + expect(popoverRef).toHaveBeenCalled() + + // Popover component's public ref prop + expect(popoverRef.mock.calls[0][0].ref).toBe(popoverRoot) + }) + + it('should give back the Popover component when drilldown has trigger and is open', async () => { + const popoverRef = vi.fn() + const { container } = render( + Toggle} + defaultShow + > + + Option + + + ) + const trigger = screen.getByText('Toggle') + const positionId = trigger.getAttribute('data-position-target') + const popoverRoot = container.querySelector( + `span[data-position="${positionId}"]` + ) + + expect(popoverRoot).toBeInTheDocument() + expect(popoverRef).toHaveBeenCalled() + + // Popover component's public ref prop + expect(popoverRef.mock.calls[0][0].ref).toBe(popoverRoot) + }) + }) + + describe('onSelect prop', () => { + it('should fire when option is selected', async () => { + const onSelect = vi.fn() + render( + + + {data.map((option) => ( + + {option.label} + + ))} + + + ) + const option_1 = screen.getByTestId('opt_1') + + await userEvent.click(option_1) + + await waitFor(() => { + expect(onSelect).toHaveBeenCalled() + + const args = onSelect.mock.calls[0][1] + const event = onSelect.mock.calls[0][0] + + expect(args.value).toBe('opt_1') + expect(args.isSelected).toBe(true) + + expect(args.selectedOption.props).toHaveProperty('id', 'opt_1') + expect(args.selectedOption.props).toHaveProperty('role', 'menuitem') + expect(args.selectedOption.props).toHaveProperty('value', 'opt_1') + + expect(args.drilldown.props).toHaveProperty('role', 'menu') + expect(args.drilldown.hide).toBeInstanceOf(Function) + + expect(event.target).toBe(option_1) + }) + }) + + it('should not fire when drilldown is disabled', async () => { + const onSelect = vi.fn() + render( + + + {data.map((option) => ( + + {option.label} + + ))} + + + ) + const option_1 = screen.getByTestId('opt_1') + + await userEvent.click(option_1) + + await waitFor(() => { + expect(onSelect).not.toHaveBeenCalled() + }) + }) + }) + + describe('with a trigger', () => { + it('should not show content by default', async () => { + render( + click me}> + + Option 0 + + + ) + const option_0 = screen.queryByText('Option 0') + + expect(option_0).not.toBeInTheDocument() + }) + + it('should render into a mountNode', async () => { + const container = document.createElement('div') + container.setAttribute('data-testid', 'container') + document.body.appendChild(container) + + render( + Options} + > + + Option 0 + + + ) + const optionsContainer = screen.getByTestId('container') + const trigger = screen.getByRole('button') + + expect(optionsContainer).not.toHaveTextContent('Option 0') + + await userEvent.click(trigger) + + await waitFor(() => { + const updatedOptionsContainer = screen.getByTestId('container') + + expect(updatedOptionsContainer).toHaveTextContent('Option 0') + }) + }) + + it('should have an aria-haspopup attribute', async () => { + render( + Options}> + + Option 0 + + + ) + const trigger = screen.getByRole('button') + + expect(trigger).toHaveAttribute('aria-haspopup') + }) + + it('should call onToggle when Drilldown is opened', async () => { + const onToggle = vi.fn() + render( + Options} + onToggle={onToggle} + data-testid="drilldown" + > + + Option 0 + + + ) + const trigger = screen.getByRole('button') + + await userEvent.click(trigger) + + await waitFor(() => { + expect(onToggle).toHaveBeenCalled() + + const args = onToggle.mock.calls[0][1] + + expect(args).toHaveProperty('shown', true) + expect(args).toHaveProperty('pageHistory', ['page0']) + + expect(args.goToPage).toBeInstanceOf(Function) + expect(args.goToPreviousPage).toBeInstanceOf(Function) + + expect(args.drilldown.props).toHaveProperty('role', 'menu') + expect(args.drilldown.props).toHaveProperty('data-testid', 'drilldown') + }) + }) + + it('should call onToggle when Drilldown is closed', async () => { + const onToggle = vi.fn() + render( + Options} + onToggle={onToggle} + defaultShow + > + + Option 0 + + + ) + const trigger = screen.getByRole('button') + + await userEvent.click(trigger) + + await waitFor(() => { + expect(onToggle).toHaveBeenCalled() + + const args = onToggle.mock.calls[0][1] + + expect(args).toHaveProperty('shown', false) + expect(args).toHaveProperty('pageHistory', ['page0']) + expect(args).toHaveProperty('drilldown') + + expect(args.goToPage).toBeInstanceOf(Function) + expect(args.goToPreviousPage).toBeInstanceOf(Function) + }) + }) + }) + + describe('placement prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + defaultShow + popoverRef={(e) => { + popoverRef = e + }} + placement={'top start'} + > + + Option + + + ) + const popoverProps = popoverRef!.props + + expect(popoverProps.placement).toBe('top start') + }) + }) + + describe('defaultShow prop', () => { + it('should display Popover on render', async () => { + render( + Toggle} + defaultShow + > + + Option + + + ) + const popoverContent = screen.getByText('Option') + + expect(popoverContent).toBeVisible() + }) + }) + + describe('show prop', () => { + it('should display popover', async () => { + const onToggle = vi.fn() + render( + Toggle} + show + onToggle={onToggle} + > + + Option + + + ) + const popoverContent = screen.getByText('Option') + + expect(popoverContent).toBeVisible() + }) + + it('should give error if onToggle is not provided (controllable)', async () => { + render( + Toggle} show> + + Option + + + ) + const popoverContent = screen.getByText('Option') + + expect(popoverContent).toBeVisible() + + expect(popoverContent).toBeVisible() + expect(consoleErrorMock).toHaveBeenCalled() + }) + }) + + describe('onFocus prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + const onFocus = vi.fn() + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + onFocus={onFocus} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.onFocus).toEqual(onFocus) + }) + }) + + describe('onMouseOver prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + const onMouseOver = vi.fn() + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + onMouseOver={onMouseOver} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.onMouseOver).toEqual(onMouseOver) + }) + }) + + describe('shouldContainFocus prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + shouldContainFocus={true} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.shouldContainFocus).toEqual(true) + }) + }) + + describe('shouldReturnFocus prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + shouldReturnFocus={false} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.shouldReturnFocus).toEqual(false) + }) + }) + + describe('withArrow prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + withArrow={false} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.withArrow).toEqual(false) + }) + }) + + describe('offsetX prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + offsetX={'2rem'} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.offsetX).toEqual('2rem') + }) + }) + + describe('offsetY prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + offsetY={'2rem'} + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.offsetY).toEqual('2rem') + }) + }) + + describe('positionContainerDisplay prop', () => { + it('should be passed to Popover', async () => { + let popoverRef: Popover | null = null + render( + Toggle} + popoverRef={(e) => { + popoverRef = e + }} + positionContainerDisplay="block" + > + + Option + + + ) + + const popoverProps = popoverRef!.props + + expect(popoverProps.positionContainerDisplay).toEqual('block') + }) + }) + + describe('for a11y', () => { + it('should be accessible', async () => { + const { container } = render( + + + Item1 + + Item2 + + + Item3 + + + Item4 + + + Item5 + + + Item6 + + } + > + Item7 + + }> + Item8 + + + + + + GroupItem + GroupItem + GroupItem + + + + GroupItem + GroupItem + GroupItem + + + + + Item1 + + + ) + + // axe-check is more strict now, and expects "list" role to have "listitem" children, but we use "role='none'" children. After discussing it with the A11y team, we agreed to ignore this error because the screen readers can read the component perfectly. + // TODO: try to remove this ignore if axe-check is updated and isn't this strict anymore + // https://dequeuniversity.com/rules/axe/4.6/aria-required-children?application=axeAPI + const axeCheck = await runAxeCheck(container, { + ignores: ['aria-required-children'] + }) + + expect(axeCheck).toBe(true) + }) + + it('should meet a11y standarts when drilldown is open', async () => { + const { container } = render( + + + Option 0 + + + ) + const axeCheck = await runAxeCheck(container) + + expect(axeCheck).toBe(true) + }) + }) + + describe('with generated examples', () => { + const generatedComponents = generateA11yTests(Drilldown, DrilldownExamples) + + it.each(generatedComponents)( + 'should be accessible with example: $description', + async ({ content }) => { + const { container } = render(content) + const axeCheck = await runAxeCheck(container, { + // axe-check is more strict now, and expects "list" role to have "listitem" children, but we use "role='none'" children. After discussing it with the A11y team, we agreed to ignore this error because the screen readers can read the component perfectly. + // TODO: try to remove this ignore if axe-check is updated and isn't this strict anymore + // https://dequeuniversity.com/rules/axe/4.6/aria-required-children?application=axeAPI + ignores: ['aria-required-children'] + }) + expect(axeCheck).toBe(true) + } + ) + }) +}) diff --git a/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx b/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx deleted file mode 100644 index ded87feaee..0000000000 --- a/packages/ui-drilldown/src/Drilldown/__tests__/Drilldown.test.tsx +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' - -import { - expect, - generateA11yTests, - match, - mount, - stub, - wrapQueryResult -} from '@instructure/ui-test-utils' - -import { IconCheckSolid } from '@instructure/ui-icons' -import { Popover } from '@instructure/ui-popover' - -import { Drilldown } from '../index' -import { DrilldownLocator } from '../DrilldownLocator' -import DrilldownExamples from '../__examples__/Drilldown.examples' - -const data = Array(5) - .fill(0) - .map((_v, ind) => ({ - label: `option ${ind}`, - id: `opt_${ind}` - })) - -const renderOptions = (page: string) => { - return data.map((option) => ( - - {option.label} - {page} - - )) -} - -describe('', async () => { - describe('rootPageId prop', async () => { - it('should set the initial page and render it', async () => { - await mount( - - - Option - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const page0Option = await drilldown.find('#option01', { - expectEmpty: true - }) - const page1Option = await drilldown.find('#option11') - - expect(page0Option).to.not.exist() - expect(page1Option).to.exist() - }) - }) - - describe('children prop', async () => { - it('should not allow non-DrilldownPage children', async () => { - stub(console, 'error') - await mount( - - DIV - - ) - - const drilldown = await DrilldownLocator.find({ - expectEmpty: true - }) - - expect(drilldown).to.not.exist() - }) - }) - - describe('id prop', async () => { - it('should put id attr on the drilldown', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - - expect(drilldown.getId()).to.equal('testId') - }) - }) - - describe('label prop', async () => { - it('should be added as aria-label', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - - expect(drilldown.getAttribute('aria-label')).to.equal('testLabel') - }) - }) - - describe('disabled prop', async () => { - it('should disable all options', async () => { - await mount( - - - Option - Option - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const allOptions = await drilldown.findAllOptions() - - expect(allOptions.length).to.equal(4) // header action + 3 options - - allOptions.forEach((option) => { - expect(option.getAttribute('aria-disabled')).to.equal('true') - }) - }) - - it('should prevent option actions', async () => { - await mount( - - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const page0option = await drilldown.find('#page0option') - - await page0option.click() - - const page1option = await drilldown.find('#page1option', { - expectEmpty: true - }) - - expect(page0option).to.be.visible() - expect(page1option).to.not.be.visible() - }) - - it('should disabled trigger, if provided', async () => { - await mount( - Toggle} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const toggleButton = await drilldown.find('[data-test-id="toggleButton"]') - - expect(toggleButton.getAttribute('aria-disabled')).to.equal('true') - expect( - (toggleButton.getDOMNode() as HTMLButtonElement).disabled - ).to.equal(true) - - await toggleButton.click() - - const option = await drilldown.find('#page0option', { expectEmpty: true }) - - expect(option).to.not.be.visible() - }) - }) - - describe('rotateFocus prop', async () => { - it('should rotate focus in the drilldown by default', async () => { - await mount( - - - Option - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const option1 = await drilldown.find('#option01') - const option2 = await drilldown.find('#option02') - - await drilldown.focus() - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option1.getDOMNode()) - - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option2.getDOMNode()) - - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option1.getDOMNode()) - }) - - it('should prevent focus rotation in the drilldown with "false"', async () => { - await mount( - - - Option - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const option1 = await drilldown.find('#option01') - const option2 = await drilldown.find('#option02') - - await drilldown.focus() - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option1.getDOMNode()) - - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option2.getDOMNode()) - - await drilldown.keyDown('down') - - expect(document.activeElement).to.equal(option2.getDOMNode()) - }) - }) - - describe('as prop', async () => { - it('should be "ul" by default', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const drilldownContainer = await drilldown.findSelectableContainer() - - expect(drilldownContainer.getDOMNode()).to.have.tagName('ul') - }) - - it('should render as passed element', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const drilldownContainer = await drilldown.findSelectableContainer() - - expect(drilldownContainer.getDOMNode()).to.have.tagName('ol') - }) - }) - - describe('role prop', async () => { - it('should be "menu" by default', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - - expect(drilldown.getAttribute('role')).to.equal('menu') - }) - - it('should apply passed role', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - - expect(drilldown.getAttribute('role')).to.equal('list') - }) - }) - - describe('elementRef prop (and ref static prop)', async () => { - it('should give back the drilldown element when there is no trigger', async () => { - const ref = React.createRef() - const elementRef = stub() - await mount( - - - Option - - , - { - props: { ref } - } - ) - - const drilldown = await DrilldownLocator.find() - - expect(ref.current).to.be.not.null() - expect(ref.current!.ref).to.be.eq(drilldown.getDOMNode()) - - expect(elementRef).to.have.been.calledWith(drilldown.getDOMNode()) - expect(elementRef.args[0][0]).to.have.attribute('role', 'menu') - }) - - it('should give back the Popover root when drilldown has trigger and is closed', async () => { - const ref = React.createRef() - const elementRef = stub() - await mount( - Toggle} - > - - Option - - , - { - props: { ref } - } - ) - - const drilldown = await DrilldownLocator.find() - const popoverRoot = await drilldown.findPopoverRoot() - - expect(ref.current).to.be.not.null() - expect(ref.current!.ref).to.be.eq(popoverRoot.getDOMNode()) - - expect(elementRef).to.have.been.calledWith(popoverRoot.getDOMNode()) - }) - - it('should give back the the Popover root when drilldown has trigger and is open', async () => { - const ref = React.createRef() - const elementRef = stub() - await mount( - Toggle} - defaultShow - > - - Option - - , - { - props: { ref } - } - ) - - const drilldown = await DrilldownLocator.find() - const popoverRoot = await drilldown.findPopoverRoot() - - expect(ref.current).to.be.not.null() - expect(ref.current!.ref).to.be.eq(popoverRoot.getDOMNode()) - - expect(elementRef).to.have.been.calledWith(popoverRoot.getDOMNode()) - }) - }) - - describe('drilldownRef prop', async () => { - it('should give back the drilldown element when there is no trigger', async () => { - const drilldownRef = stub() - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - - expect(drilldownRef).to.have.been.calledWith(drilldown.getDOMNode()) - expect(drilldownRef.args[0][0]).to.have.attribute('role', 'menu') - }) - - it("shouldn't be called when drilldown has trigger and is closed", async () => { - const drilldownRef = stub() - await mount( - Toggle} - > - - Option - - - ) - - expect(drilldownRef).to.not.have.been.called() - }) - - it('should give back the drilldown element when drilldown has trigger and is open', async () => { - const drilldownRef = stub() - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const drilldownMenu = await popoverContent.find('[role="menu"]') - - expect(drilldownRef).to.have.been.calledWith(drilldownMenu.getDOMNode()) - }) - }) - - describe('popoverRef prop', async () => { - it('should not be called when there is no trigger', async () => { - const popoverRef = stub() - await mount( - - - Option - - - ) - - expect(popoverRef).to.not.have.been.called() - }) - - it('should give back the Popover component when drilldown has trigger and is closed', async () => { - const popoverRef = stub() - await mount( - Toggle} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverRoot = await drilldown.findPopoverRoot() - - expect(popoverRef).to.have.been.called() - // Popover component's public ref prop - expect(popoverRef.args[0][0].ref).to.equal(popoverRoot.getDOMNode()) - }) - - it('should give back the Popover component when drilldown has trigger and is open', async () => { - const popoverRef = stub() - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverRoot = await drilldown.findPopoverRoot() - - expect(popoverRef).to.have.been.called() - // Popover component's public ref prop - expect(popoverRef.args[0][0].ref).to.equal(popoverRoot.getDOMNode()) - }) - }) - - describe('width prop', async () => { - it('should set the width of the drilldown', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.width).to.equal('320px') - }) - - it('should set the width of the drilldown in the popover', async () => { - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const container = await popoverContent.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.width).to.equal('320px') - }) - - it('should be overruled by maxWidth prop', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.width).to.equal('160px') - }) - - it('should be affected by overflowX prop', async () => { - await mount( - - - - - Option with a very long label so that it has to break - - - - - ) - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerNode = container.getDOMNode() - const containerStyle = getComputedStyle(containerNode) - - expect(containerStyle.width).to.equal('320px') - expect(containerStyle.overflowX).to.equal('auto') - expect(containerNode.scrollWidth > containerNode.clientWidth).to.equal( - true - ) - }) - }) - - describe('minWidth prop', async () => { - it('should set minWidth in popover mode', async () => { - await mount( - Trigger} - show - onToggle={stub()} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const container = await popoverContent.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.width).to.equal('336px') - }) - }) - - describe('height prop', async () => { - it('should set the height of the drilldown', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.height).to.equal('320px') - }) - - it('should set the height of the drilldown in the popover', async () => { - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const container = await popoverContent.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.height).to.equal('320px') - }) - - it('should be overruled by maxHeight prop', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.height).to.equal('160px') - }) - - it('should be affected by overflowY prop', async () => { - await mount( - - - Option - Option - Option - Option - Option - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerNode = container.getDOMNode() - const containerStyle = getComputedStyle(containerNode) - - expect(containerStyle.height).to.equal('160px') - expect(containerStyle.overflowY).to.equal('auto') - expect(containerNode.scrollHeight > containerNode.clientHeight).to.equal( - true - ) - }) - }) - - describe('minHeight prop', async () => { - it('should set height', async () => { - await mount( - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const container = await drilldown.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.height).to.equal('336px') - }) - - it('should set height in popover mode', async () => { - await mount( - Trigger} - show - onToggle={stub()} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const container = await popoverContent.findSizableContainer() - const containerStyle = getComputedStyle(container.getDOMNode()) - - expect(containerStyle.height).to.equal('336px') - }) - }) - - describe('onSelect prop', async () => { - it('should fire when option is selected', async () => { - const onSelect = stub() - await mount( - - - {data.map((option) => ( - - {option.label} - - ))} - - - ) - const drilldown = await DrilldownLocator.find() - const option = await drilldown.findAllOptions() - await option[1].click() - - expect(onSelect).to.have.been.called() - const selectValue = match.falsy.or(match.string).or(match.number) - expect(onSelect).to.have.been.calledWithMatch(match.object, { - value: match.every(selectValue).or(selectValue), - isSelected: match.bool, - selectedOption: match.object, - drilldown: match.object - }) - - // 1st arg is the event - expect(onSelect.lastCall.args[0].target).to.equal(option[1].getDOMNode()) - - expect(onSelect.lastCall.args[1].selectedOption.props.value).to.equal( - 'opt_1' - ) - expect(typeof onSelect.lastCall.args[1].drilldown.hide).to.equal( - 'function' - ) - }) - - it('should not fire when drilldown is disabled', async () => { - const onSelect = stub() - await mount( - - - {data.map((option) => ( - - {option.label} - - ))} - - - ) - const drilldown = await DrilldownLocator.find() - const option = await drilldown.findAllOptions() - await option[1].click() - expect(onSelect).to.not.have.been.called() - }) - }) - - describe('with a trigger', () => { - it('should not show content by default', async () => { - await mount( - click me}> - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - const trigger = await drilldown.findPopoverTrigger() - const content = await drilldown.findPopoverContent({ - expectEmpty: true - }) - - expect(trigger.getTextContent()).to.be.eq('click me') - expect(content).to.be.null() - }) - - it('should render into a mountNode', async () => { - const container = document.createElement('div') - document.body.appendChild(container) - - await mount( - Options} - > - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - const trigger = await drilldown.findPopoverTrigger() - - await trigger.click() - - expect(container.innerText).to.eq('Option 0') - }) - - it('should have an aria-haspopup attribute', async () => { - await mount( - Options}> - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - const trigger = await drilldown.findPopoverTrigger() - - expect(trigger.getAttribute('aria-haspopup')).to.exist() - }) - - it('should call onToggle when Drilldown is opened', async () => { - const onToggle = stub() - await mount( - Options} - onToggle={onToggle} - > - - Option 0 - - - ) - - const drilldown = await DrilldownLocator.find() - const trigger = await drilldown.findPopoverTrigger() - - await trigger.click() - - expect(onToggle).to.have.been.called() - expect(onToggle).to.have.been.calledWithMatch(match.object, { - shown: true, - drilldown: match.object, - pageHistory: ['page0'], - goToPage: match.func, - goToPreviousPage: match.func - }) - }) - - it('should call onToggle when Drilldown is closed', async () => { - const onToggle = stub() - await mount( - Options} - onToggle={onToggle} - defaultShow - > - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - await drilldown.keyUp('esc') - - expect(onToggle).to.have.been.called() - expect(onToggle).to.have.been.calledWithMatch(match.instanceOf(Event), { - shown: false, - drilldown: match.object, - pageHistory: ['page0'], - goToPage: match.func, - goToPreviousPage: match.func - }) - }) - - it('should call onDismiss when Drilldown is closed', async () => { - const onDismiss = stub() - await mount( - Options} - onDismiss={onDismiss} - defaultShow - > - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - - await drilldown.keyUp(27) - - expect(onDismiss).to.have.been.called() - expect(onDismiss).to.have.been.calledWithMatch( - match.instanceOf(Event), - false - ) - }) - }) - - describe('placement prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - defaultShow - popoverRef={(e) => { - popoverRef = e - }} - placement={'top start'} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.placement).to.equal('top start') - }) - }) - - describe('defaultShow prop', async () => { - it('should display Popover on render', async () => { - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - - expect(popoverContent).to.be.visible() - }) - }) - - describe('show prop', async () => { - it('should display popover', async () => { - const onToggle = stub() - await mount( - Toggle} - show - onToggle={onToggle} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - - expect(popoverContent).to.be.visible() - }) - - // TODO: this test is flaky and randomly breaks, try to fix later - // it('should give error if onToggle is not provided (controllable)', async () => { - // const consoleError = stub(console, 'error') - // await mount( - // Toggle} show> - // - // Option - // - // - // ) - // - // const drilldown = await DrilldownLocator.find() - // const popoverContent = await drilldown.findPopoverContent() - // - // expect(popoverContent).to.be.visible() - // expect(consoleError).to.have.been.called() - // }) - }) - - describe('onFocus prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - const onFocus = stub() - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - onFocus={onFocus} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.onFocus).to.equal(onFocus) - }) - }) - - describe('onMouseOver prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - const onMouseOver = stub() - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - onMouseOver={onMouseOver} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.onMouseOver).to.equal(onMouseOver) - }) - }) - - describe('shouldContainFocus prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - shouldContainFocus={true} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.shouldContainFocus).to.equal(true) - }) - }) - - describe('shouldReturnFocus prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - shouldReturnFocus={false} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.shouldReturnFocus).to.equal(false) - }) - }) - - describe('withArrow prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - withArrow={false} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.withArrow).to.equal(false) - }) - }) - - describe('offsetX prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - offsetX={'2rem'} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.offsetX).to.equal('2rem') - }) - }) - - describe('offsetY prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - offsetY={'2rem'} - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.offsetY).to.equal('2rem') - }) - }) - - describe('positionContainerDisplay prop', async () => { - it('should be passed to Popover', async () => { - let popoverRef: Popover | null = null - await mount( - Toggle} - popoverRef={(e) => { - popoverRef = e - }} - positionContainerDisplay="block" - > - - Option - - - ) - - const popoverProps = popoverRef!.props - - expect(popoverProps.positionContainerDisplay).to.equal('block') - }) - }) - - describe('shouldHideOnSelect prop', async () => { - it('should be true by default', async () => { - await mount( - Toggle} - defaultShow - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const option = await popoverContent.find('#option01') - - await option.click() - - expect(popoverContent).to.not.be.visible() - }) - - it('should not close on subPage nav, even if "true"', async () => { - await mount( - Toggle} - defaultShow - shouldHideOnSelect={true} - > - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const option = await popoverContent.find('#option01') - - await option.click() - - expect(popoverContent).to.be.visible() - expect(await popoverContent.find('#option11')).to.be.visible() - }) - - it('should not close on Back nav, even if "true"', async () => { - await mount( - Toggle} - defaultShow - shouldHideOnSelect={true} - > - - - Option - - - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const option = await popoverContent.find('#option01') - - await option.click() - - const backNav = await popoverContent.findHeaderBackOption() - - await backNav.click() - - expect(popoverContent).to.be.visible() - expect(await popoverContent.find('#option01')).to.be.visible() - }) - - it('should prevent closing when "false"', async () => { - await mount( - Toggle} - defaultShow - shouldHideOnSelect={false} - > - - Option - - - ) - - const drilldown = await DrilldownLocator.find() - const popoverContent = await drilldown.findPopoverContent() - const option = await popoverContent.find('#option01') - - await option.click() - - expect(popoverContent).to.be.visible() - expect(option).to.be.visible() - }) - }) - - describe('navigation', () => { - it('should be able to navigate between options with up/down arrows', async () => { - await mount( - - - {data.map((option) => ( - - {option.label} - - ))} - - - ) - const drilldown = await DrilldownLocator.find() - const options = await drilldown.findAllOptions() - - await drilldown.focus() - await drilldown.keyDown('down') - - expect(options[0].containsFocus()).to.be.true() - - await drilldown.keyDown('down') - - expect(options[1].containsFocus()).to.be.true() - - await drilldown.keyDown('down') - - expect(options[2].containsFocus()).to.be.true() - - await drilldown.keyDown('up') - expect(options[1].containsFocus()).to.be.true() - }) - - it('should be able to navigate forward between pages with right arrow', async () => { - await mount( - - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - const drilldown = await DrilldownLocator.find() - let options = await drilldown.findAllOptionWrappers() - - await drilldown.focus() - await drilldown.keyDown('down') - // the option which navigates to next page should be focused - expect(options[1].containsFocus()).to.be.true() - - // options[1] is the active element - let activeOption: any = wrapQueryResult(document.activeElement!) - await activeOption.keyDown('ArrowRight') - - // the 1st option is the `Back` button, navigate to the second option - await Promise.all([drilldown.keyDown('down'), drilldown.keyDown('down')]) - let header = await drilldown.findHeaderTitle() - expect(header.text()).to.be.eq('Page 1') - options = await drilldown.findAllOptionWrappers() - activeOption = options.find((opt: any) => opt.containsFocus()) - expect(activeOption.text()).to.be.eq('To Page 2') - await activeOption.keyDown('ArrowRight') - header = await drilldown.findHeaderTitle() - expect(header.text()).to.be.eq('Page 2') - }) - - it('should be able to navigate back to previous page with left arrow', async () => { - await mount( - - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - - const drilldown = await DrilldownLocator.find() - - await drilldown.focus() - await drilldown.keyDown('down') - const activeOption = wrapQueryResult(document.activeElement!) - await activeOption.keyDown('ArrowRight') - await drilldown.keyDown('ArrowLeft') - - const header = await drilldown.findHeaderTitle() - - expect(drilldown.containsFocus()).to.be.true() - expect(header.text()).to.be.eq('Page 0') - }) - - it('should close the drilldown on root page and left arrow is pressed', async () => { - await mount( - options} - defaultShow - > - - - To Page 1 - - - - {[ - - To Page 2 - , - ...renderOptions('page 1') - ]} - - - - {renderOptions('page 2')} - - - ) - - const drilldown = await DrilldownLocator.find() - - const content = await drilldown.findPopoverContent() - const innerDrilldown = await content.find('[role=menu]') - - expect(innerDrilldown.getDOMNode()).to.be.eq(document.activeElement) - - await innerDrilldown.keyDown('ArrowLeft') - - const contentWrapper = await drilldown.findPopoverContent({ - expectEmpty: true - }) - - expect(contentWrapper).to.be.null() - expect(drilldown.containsFocus()).to.be.false() - }) - }) - - describe('for a11y', async () => { - it('should be accessible', async () => { - await mount( - - - Item1 - - Item2 - - - Item3 - - - Item4 - - - Item5 - - - Item6 - - } - > - Item7 - - }> - Item8 - - - - - - GroupItem - GroupItem - GroupItem - - - - GroupItem - GroupItem - GroupItem - - - - - Item1 - - - ) - - const drilldown = await DrilldownLocator.find() - - expect( - await drilldown.accessible({ - ignores: [ - // see explanation in Options.test.tsx - 'aria-required-children' - ] - }) - ).to.be.true() - }) - - it('should meet a11y standarts when drilldown is open', async () => { - const subject = await mount( - - - Option 0 - - - ) - const element = wrapQueryResult(subject.getDOMNode()) - expect(await element.accessible()).to.be.true() - }) - - it('should correctly return focus when "trigger" and "shouldReturnFocus" is set', async () => { - await mount( - Options} - shouldReturnFocus - > - - Option 0 - - - ) - const drilldown = await DrilldownLocator.find() - const trigger = await drilldown.findPopoverTrigger() - - await trigger.focus() - - await trigger.keyDown(' ') - await drilldown.keyDown('esc') - - expect(trigger.containsFocus()).to.be.true() - }) - }) - - describe('with generated examples', async () => { - // the `scrollable-region-focusable` axe error is not valid because - // axe does not see how the scrollable `div` can be focused - generateA11yTests(Drilldown, DrilldownExamples, [ - 'scrollable-region-focusable', - // see explanation in Options.test.tsx - 'aria-required-children' - ]) - }) -}) diff --git a/packages/ui-drilldown/tsconfig.build.json b/packages/ui-drilldown/tsconfig.build.json index abe8b6ea51..da0de3b14b 100644 --- a/packages/ui-drilldown/tsconfig.build.json +++ b/packages/ui-drilldown/tsconfig.build.json @@ -23,8 +23,9 @@ { "path": "../ui-view/tsconfig.build.json" }, { "path": "../ui-babel-preset/tsconfig.build.json" }, { "path": "../ui-color-utils/tsconfig.build.json" }, - { "path": "../ui-test-locator/tsconfig.build.json" }, { "path": "../ui-test-utils/tsconfig.build.json" }, - { "path": "../ui-themes/tsconfig.build.json" } + { "path": "../ui-themes/tsconfig.build.json" }, + { "path": "../ui-axe-check/tsconfig.build.json" }, + { "path": "../ui-scripts/tsconfig.build.json" } ] }