diff --git a/packages/react-core/src/components/Menu/__mocks__/Menu.tsx b/packages/react-core/src/components/Menu/__mocks__/Menu.tsx new file mode 100644 index 00000000000..950e47acfd8 --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/Menu.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { MenuProps } from '../Menu'; + +export const Menu = ({ className, isPlain, isScrollable, style, onSelect, ...props }: MenuProps) => ( + <> +
+
{'Mock item'}
+

{`isPlain: ${isPlain}`}

+

{`isScrollable: ${isScrollable}`}

+

{`minWidth: ${style?.['--pf-c-menu--MinWidth']}`}

+ +); diff --git a/packages/react-core/src/components/Menu/__mocks__/MenuContent.tsx b/packages/react-core/src/components/Menu/__mocks__/MenuContent.tsx new file mode 100644 index 00000000000..bd4229efbae --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/MenuContent.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const MenuContent = ({ children }) =>
{children}
; diff --git a/packages/react-core/src/components/Menu/__mocks__/MenuGroup.tsx b/packages/react-core/src/components/Menu/__mocks__/MenuGroup.tsx new file mode 100644 index 00000000000..98f34af2bca --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/MenuGroup.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { MenuGroupProps } from '../MenuGroup'; + +export const MenuGroup = ({ className, children, label, labelHeadingLevel }: MenuGroupProps) => ( + <> +
+ {children} +
+

{`label: ${label}`}

+

{`labelHeadingLevel: ${labelHeadingLevel}`}

+ +); diff --git a/packages/react-core/src/components/Menu/__mocks__/MenuItem.tsx b/packages/react-core/src/components/Menu/__mocks__/MenuItem.tsx new file mode 100644 index 00000000000..bb9c74c8b2d --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/MenuItem.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { MenuItemProps } from '../MenuItem'; + +export const MenuItem = ({ className, children, description, itemId }: MenuItemProps) => ( + <> +
+ {children} +
+

{`description: ${description}`}

+

{`itemId: ${itemId}`}

+ +); diff --git a/packages/react-core/src/components/Menu/__mocks__/MenuList.tsx b/packages/react-core/src/components/Menu/__mocks__/MenuList.tsx new file mode 100644 index 00000000000..03d5d604621 --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/MenuList.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { MenuListProps } from '../MenuList'; + +export const MenuList = ({ className, children }: MenuListProps) => ( + <> +
+ {children} +
+ +); diff --git a/packages/react-core/src/components/Menu/__mocks__/index.ts b/packages/react-core/src/components/Menu/__mocks__/index.ts new file mode 100644 index 00000000000..911c5ab0014 --- /dev/null +++ b/packages/react-core/src/components/Menu/__mocks__/index.ts @@ -0,0 +1,5 @@ +export * from './Menu'; +export * from './MenuContent'; +export * from './MenuGroup'; +export * from './MenuItem'; +export * from './MenuList'; diff --git a/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx b/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx new file mode 100644 index 00000000000..76befc60fdf --- /dev/null +++ b/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { PopperProps } from '../Popper'; + +export const Popper = ({ popper, zIndex, isVisible, trigger }: PopperProps) => ( + <> +
{popper}
+

{`zIndex: ${zIndex}`}

+

{`isOpen: ${isVisible}`}

+
{trigger}
+ +); diff --git a/packages/react-core/src/helpers/Popper/__mocks__/index.ts b/packages/react-core/src/helpers/Popper/__mocks__/index.ts new file mode 100644 index 00000000000..86443d1e3b1 --- /dev/null +++ b/packages/react-core/src/helpers/Popper/__mocks__/index.ts @@ -0,0 +1 @@ +export * from './Popper'; diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/Dropdown.test.tsx b/packages/react-core/src/next/components/Dropdown/__tests__/Dropdown.test.tsx new file mode 100644 index 00000000000..e1ff2ffe2bc --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/Dropdown.test.tsx @@ -0,0 +1,236 @@ +import React from 'react'; +import { Dropdown } from '../../Dropdown'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('../../../../components/Menu'); + +jest.mock('../../../../helpers/Popper/Popper'); + +const toggle = (ref: React.RefObject) => ; + +const dropdownChildren =
Dropdown children
; + +test('renders dropdown', () => { + render( +
+ toggle(toggleRef)}>{dropdownChildren} +
+ ); + + expect(screen.getByTestId('dropdown').children[0]).toBeVisible(); +}); + +test('passes children', () => { + render( toggle(toggleRef)}>{dropdownChildren}); + + expect(screen.getByText('Dropdown children')).toBeVisible(); +}); + +test('renders passed toggle element', () => { + render( toggle(toggleRef)}>{dropdownChildren}); + + expect(screen.getByRole('button', { name: 'Dropdown' })).toBeVisible(); +}); + +test('passes no class name by default', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByTestId('menu-mock')).not.toHaveClass(); +}); + +test('passes custom class name', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByTestId('menu-mock')).toHaveClass('custom-class'); +}); + +test('does not pass isPlain to Menu by default', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('isPlain: undefined')).toBeVisible(); +}); + +test('passes isPlain to Menu', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('isPlain: true')).toBeVisible(); +}); + +test('does not pass isScrollable to Menu by default', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('isScrollable: undefined')).toBeVisible(); +}); + +test('passes isScrollable to Menu', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('isScrollable: true')).toBeVisible(); +}); + +test('does not pass minWidth to Menu by default', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('minWidth: undefined')).toBeVisible(); +}); + +test('passes minWidth to Menu', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('minWidth: 100px')).toBeVisible(); +}); + +test('passes default zIndex to popper', () => { + render( toggle(toggleRef)}>{dropdownChildren}); + + expect(screen.getByText('zIndex: 9999')).toBeVisible(); +}); + +test('passes zIndex to popper', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('zIndex: 100')).toBeVisible(); +}); + +test('does not pass isOpen to popper by default', () => { + render( toggle(toggleRef)}>{dropdownChildren}); + + expect(screen.getByText('isOpen: undefined')).toBeVisible(); +}); + +test('passes isOpen to popper', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + expect(screen.getByText('isOpen: true')).toBeVisible(); +}); + +/* no default tests for callback props +since there is no way to test that the +function doesn`t get passed */ + +test('passes onSelect callback', async () => { + const user = userEvent.setup(); + + const onSelect = jest.fn(); + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + const trigger = await screen.findByText('Mock item'); + await user.click(trigger); + + expect(onSelect).toBeCalledTimes(1); +}); + +test('onOpenChange is called when passed and user clicks outside of dropdown', async () => { + const user = userEvent.setup(); + const onOpenChange = jest.fn(); + + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + const dropdown = screen.getByRole('button', { name: 'Dropdown' }); + await user.click(dropdown); + await user.click(document.body); + + expect(onOpenChange).toBeCalledTimes(1); +}); + +test('onOpenChange is called when passed and user presses tab key', async () => { + const user = userEvent.setup(); + const onOpenChange = jest.fn(); + + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + //focus dropdown + const dropdown = screen.getByRole('button', { name: 'Dropdown' }); + await user.click(dropdown); + await user.keyboard('{Tab}'); + + expect(onOpenChange).toBeCalledTimes(1); +}); + +test('onOpenChange is called when passed and user presses esc key', async () => { + const user = userEvent.setup(); + const onOpenChange = jest.fn(); + + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + //focus dropdown + const dropdown = screen.getByRole('button', { name: 'Dropdown' }); + await user.click(dropdown); + await user.keyboard('{Escape}'); + + expect(onOpenChange).toBeCalledTimes(1); +}); + +test('match snapshot', () => { + const { asFragment } = render( + toggle(toggleRef)} + > + {dropdownChildren} + + ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/DropdownGroup.test.tsx b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownGroup.test.tsx new file mode 100644 index 00000000000..4151c2a304c --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownGroup.test.tsx @@ -0,0 +1,69 @@ +import { DropdownGroup } from '../../Dropdown'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +jest.mock('../../../../components/Menu'); + +const dropdownGroupChildren =
Dropdown Group children
; + +test('renders dropdown group', () => { + render( +
+ {dropdownGroupChildren} +
+ ); + + expect(screen.getByTestId('dropdown-group').children[0]).toBeVisible(); +}); + +test('passes children', () => { + render({dropdownGroupChildren}); + + expect(screen.getByText('Dropdown Group children')).toBeVisible(); +}); + +test('passes no class name by default', () => { + render({dropdownGroupChildren}); + + expect(screen.getByTestId('menu-group-mock')).not.toHaveClass(); +}); + +test('passes custom class name to MenuGroup', () => { + render({dropdownGroupChildren}); + + expect(screen.getByTestId('menu-group-mock')).toHaveClass('custom-class'); +}); + +test('passes no label by default', () => { + render({dropdownGroupChildren}); + + expect(screen.getByText('label: undefined')).toBeVisible(); +}); + +test('passes custom label to MenuGroup', () => { + render({dropdownGroupChildren}); + + expect(screen.getByText('label: Test label')).toBeVisible(); +}); + +test('passes h1 as labelHeadingLevel to MenuGroup by default', () => { + render({dropdownGroupChildren}); + + expect(screen.getByText('labelHeadingLevel: h1')).toBeVisible(); +}); + +test('passes custom labelHeadingLevel to MenuGroup', () => { + render({dropdownGroupChildren}); + + expect(screen.getByText('labelHeadingLevel: h2')).toBeVisible(); +}); + +test('matches snapshot', () => { + const { asFragment } = render( + + {dropdownGroupChildren} + + ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/DropdownItem.test.tsx b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownItem.test.tsx new file mode 100644 index 00000000000..8e626ec5be5 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownItem.test.tsx @@ -0,0 +1,73 @@ +import { DropdownItem } from '../../Dropdown'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +jest.mock('../../../../components/Menu'); + +const dropdownItemChildren =
Dropdown Item children
; + +test('renders dropdown item', () => { + render( +
+ {dropdownItemChildren} +
+ ); + + expect(screen.getByTestId('dropdown-item').children[0]).toBeVisible(); +}); + +test('passes children', () => { + render({dropdownItemChildren}); + + expect(screen.getByText('Dropdown Item children')).toBeVisible(); +}); + +test('passes no class name by default', () => { + render({dropdownItemChildren}); + + expect(screen.getByTestId('menu-item-mock')).not.toHaveClass(); +}); + +test('passes custom class name to MenuItem', () => { + render({dropdownItemChildren}); + + expect(screen.getByTestId('menu-item-mock')).toHaveClass('custom-class'); +}); + +test('passes no description by default', () => { + render({dropdownItemChildren}); + + expect(screen.getByText('description: undefined')).toBeVisible(); +}); + +test('passes custom description to MenuItem', () => { + render({dropdownItemChildren}); + + expect(screen.getByText('description: Test description')).toBeVisible(); +}); + +test('passes no itemId by default', () => { + render({dropdownItemChildren}); + + expect(screen.getByText('itemId: undefined')); +}); + +test('passes itemId to MenuItem', () => { + render( + + {dropdownItemChildren} + + ); + + expect(screen.getByText('itemId: dropdown item')); +}); + +test('matches snapshot', () => { + const { asFragment } = render( + + {dropdownItemChildren} + + ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/DropdownList.test.tsx b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownList.test.tsx new file mode 100644 index 00000000000..9599158b8a4 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/DropdownList.test.tsx @@ -0,0 +1,41 @@ +import { DropdownList } from '../../Dropdown'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +jest.mock('../../../../components/Menu'); + +const dropdownListChildren =
Dropdown List children
; + +test('renders dropdown list', () => { + render( +
+ {dropdownListChildren} +
+ ); + + expect(screen.getByTestId('dropdown-list').children[0]).toBeVisible(); +}); + +test('passes children', () => { + render({dropdownListChildren}); + + expect(screen.getByText('Dropdown List children')).toBeVisible(); +}); + +test('passes no class name by default', () => { + render({dropdownListChildren}); + + expect(screen.getByTestId('menu-list-mock')).not.toHaveClass(); +}); + +test('passes custom class name to MenuList', () => { + render({dropdownListChildren}); + + expect(screen.getByTestId('menu-list-mock')).toHaveClass('custom-class'); +}); + +test('matches snapshot', () => { + const { asFragment } = render({dropdownListChildren}); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap new file mode 100644 index 00000000000..b6e17bbdd28 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`match snapshot 1`] = ` + +
+
+
+
+
+ Dropdown children +
+
+
+
+ Mock item +
+

+ isPlain: true +

+

+ isScrollable: true +

+

+ minWidth: undefined +

+
+

+ zIndex: 9999 +

+

+ isOpen: true +

+
+ +
+
+
+`; diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownGroup.test.tsx.snap b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownGroup.test.tsx.snap new file mode 100644 index 00000000000..12277b3b888 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownGroup.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + +
+
+ Dropdown Group children +
+
+

+ label: Test label +

+

+ labelHeadingLevel: h2 +

+
+`; diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownItem.test.tsx.snap b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownItem.test.tsx.snap new file mode 100644 index 00000000000..b814290d162 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownItem.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + +
+
+ Dropdown Item children +
+
+

+ description: Test description +

+

+ itemId: undefined +

+
+`; diff --git a/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownList.test.tsx.snap b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownList.test.tsx.snap new file mode 100644 index 00000000000..1e25ffb9bf4 --- /dev/null +++ b/packages/react-core/src/next/components/Dropdown/__tests__/__snapshots__/DropdownList.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + +
+
+ Dropdown List children +
+
+
+`; diff --git a/packages/react-integration/cypress/integration/dropdownnext.spec.ts b/packages/react-integration/cypress/integration/dropdownnext.spec.ts new file mode 100644 index 00000000000..b34861418cd --- /dev/null +++ b/packages/react-integration/cypress/integration/dropdownnext.spec.ts @@ -0,0 +1,45 @@ +describe('Dropdown next demo test', () => { + it('navigate to demo section', () => { + cy.visit('http://localhost:3000/dropdown-next-demo-nav-link'); + }); + + // mouse interactions + it('opens/closes dropdown menu when clicked', () => { + cy.get('[data-cy="toggle"]').click(); + cy.get('[data-cy="toggle"]').should('have.class', 'pf-m-expanded'); + cy.get('[data-cy="toggle"]').click(); + cy.get('[data-cy="toggle"]').should('not.have.class', 'pf-m-expanded'); + }); + + it('closes dropdown when clicked outside', () => { + cy.get('[data-cy="toggle"]').click(); + cy.get('main').click(0, 0); + cy.get('[data-cy="toggle"]').should('not.have.class', 'pf-m-expanded'); + }); + + it('closes dropdown when dropdown item clicked', () => { + cy.get('[data-cy="toggle"]').click(); + cy.get('[data-cy="dropdown-item"]').click(); + cy.get('[data-cy="toggle"]').should('not.have.class', 'pf-m-expanded'); + }); + + // keyboard interactions + it('closes dropdown on pressing esc, focus stays on the toggle', () => { + cy.get('[data-cy="toggle"]').click(); + cy.get('[data-cy="toggle"]').trigger('keydown', { key: 'Escape' }); + cy.get('[data-cy="toggle"]').should('be.focused'); + cy.get('[data-cy="toggle"]').should('not.have.class', 'pf-m-expanded'); + }); + + it('closes dropdown on pressing tab, focus stays on the toggle', () => { + cy.get('[data-cy="toggle"]').click(); + cy.get('[data-cy="toggle"]').trigger('keydown', { key: 'Tab' }); + cy.get('[data-cy="toggle"]').should('be.focused'); + cy.get('[data-cy="toggle"]').should('not.have.class', 'pf-m-expanded'); + }); + + /* + pressing enter or space key on a button calls a click event internally + so testing for a button click should be sufficient + */ +}); diff --git a/packages/react-integration/demo-app-ts/src/Demos.ts b/packages/react-integration/demo-app-ts/src/Demos.ts index 0c1528830c6..a470194890e 100644 --- a/packages/react-integration/demo-app-ts/src/Demos.ts +++ b/packages/react-integration/demo-app-ts/src/Demos.ts @@ -142,6 +142,11 @@ export const Demos: DemoInterface[] = [ name: 'Dropdown Demo', componentType: Examples.DropdownDemo }, + { + id: 'dropdown-next-demo', + name: 'Dropdown Next Demo', + componentType: Examples.DropdownNextDemo + }, { id: 'dual-list-selector-basic-demo', name: 'DualListSelector basic Demo', diff --git a/packages/react-integration/demo-app-ts/src/components/demos/DropdownNextDemo/DropdownNextDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/DropdownNextDemo/DropdownNextDemo.tsx new file mode 100644 index 00000000000..07ba7a24afc --- /dev/null +++ b/packages/react-integration/demo-app-ts/src/components/demos/DropdownNextDemo/DropdownNextDemo.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Dropdown, DropdownList, DropdownItem } from '@patternfly/react-core/dist/esm/next'; +import { Divider, MenuToggle } from '@patternfly/react-core'; + +const dropDownItems = ( + + + Link + + ev.preventDefault()} + > + Action + + + Disabled link + + + Disabled action + + + + Separated link + + + Separated action + + +); + +export const DropdownNextDemo: React.FunctionComponent = () => { + const [isOpen, setIsOpen] = React.useState(false); + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onSelect = (_event: React.MouseEvent | undefined) => { + setIsOpen(false); + }; + + return ( + setIsOpen(isOpen)} + onSelect={onSelect} + toggle={toggleRef => ( + + Dropdown + + )} + > + {dropDownItems} + + ); +}; +DropdownNextDemo.displayName = 'DropdownNextDemo'; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/index.ts b/packages/react-integration/demo-app-ts/src/components/demos/index.ts index e1a2e201e59..949a44f26f0 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/index.ts +++ b/packages/react-integration/demo-app-ts/src/components/demos/index.ts @@ -25,6 +25,7 @@ export * from './ToolbarDemo/ToolbarVisibilityDemo'; export * from './DrawerDemo/DrawerDemo'; export * from './DrawerDemo/DrawerResizeDemo'; export * from './DropdownDemo/DropdownDemo'; +export * from './DropdownNextDemo/DropdownNextDemo'; export * from './DualListSelectorDemo/DualListSelectorBasicDemo'; export * from './DualListSelectorDemo/DualListSelectorTreeDemo'; export * from './DualListSelectorDemo/DualListSelectorWithActionsDemo';