Skip to content

Commit

Permalink
Navigation: add controlled unit test, use modern Testing Library fe…
Browse files Browse the repository at this point in the history
…atures (WordPress#41668)

* Rewrite tests using user-event and modern testing library asseertions, remove unnecessary async

* Add controlled test

* CHANGELOG

* Fix typo

* Check for `aria-current` attribute instead of `is-active` classname
  • Loading branch information
ciampo authored Jun 15, 2022
1 parent f037c18 commit a8c8b14
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 53 deletions.
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
- `ExternalLink`: Convert to TypeScript ([#41681](https://github.com/WordPress/gutenberg/pull/41681)).
- `InputControl` updated to satisfy `react/exhuastive-deps` eslint rule ([#41601](https://github.com/WordPress/gutenberg/pull/41601))

### Experimental

- `Navigation`: improve unit tests by using `@testing-library/user-event` and modern `@testing-library` assertions; add unit test for controlled component ([#41668](https://github.com/WordPress/gutenberg/pull/41668)).

## 19.12.0 (2022-06-01)

### Bug Fix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ControlledStateStory() {
const MockLink = ( { href, children } ) => (
<Button
href={ href }
// Since we're not actually navigating pages, simulate it with on onClick.
// Since we're not actually navigating pages, simulate it with onClick.
onClick={ ( event ) => {
event.preventDefault();
const item = href.replace( 'https://example.com/', '' );
Expand Down
304 changes: 252 additions & 52 deletions packages/components/src/navigation/test/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -10,7 +16,7 @@ import Navigation from '..';
import NavigationItem from '../item';
import NavigationMenu from '../menu';

const testNavigation = ( { activeItem, rootTitle, showBadge } = {} ) => (
const TestNavigation = ( { activeItem, rootTitle, showBadge } = {} ) => (
<Navigation activeItem={ activeItem }>
<NavigationMenu title={ rootTitle }>
<NavigationItem
Expand Down Expand Up @@ -45,92 +51,286 @@ const testNavigation = ( { activeItem, rootTitle, showBadge } = {} ) => (
</Navigation>
);

const TestNavigationControlled = () => {
const [ activeItem, setActiveItem ] = useState( 'item-1' );
const [ activeMenu, setActiveMenu ] = useState( 'root' );

const onMockLinkClick = ( event ) => {
event.preventDefault();
const item = event.target.href.replace( 'https://example.com/', '' );
setActiveItem( item );
};

return (
<>
<Navigation
activeItem={ activeItem }
activeMenu={ activeMenu }
className="navigation-story"
onActivateMenu={ setActiveMenu }
>
<NavigationMenu title="Home">
<NavigationItem
item="item-1"
title="Item 1"
href="https://example.com/item-1"
onClick={ onMockLinkClick }
/>
<NavigationItem
item="item-2"
title="Item 2"
href="https://example.com/item-2"
onClick={ onMockLinkClick }
/>
<NavigationItem
item="item-sub-menu"
navigateToMenu="sub-menu"
title="Sub-Menu"
/>
</NavigationMenu>
<NavigationMenu
menu="sub-menu"
parentMenu="root"
title="Sub-Menu"
>
<NavigationItem
item="child-1"
onClick={ () => setActiveItem( 'child-1' ) }
title="Child 1"
/>
<NavigationItem
item="child-2"
onClick={ () => setActiveItem( 'child-2' ) }
title="Child 2"
/>
<NavigationItem
item="child-nested-sub-menu"
navigateToMenu="nested-sub-menu"
title="Nested Sub-Menu"
/>
</NavigationMenu>
<NavigationMenu
menu="nested-sub-menu"
parentMenu="sub-menu"
title="Nested Sub-Menu"
>
<NavigationItem
item="sub-child-1"
onClick={ () => setActiveItem( 'sub-child-1' ) }
title="Sub-Child 1"
/>
<NavigationItem
item="sub-child-2"
onClick={ () => setActiveItem( 'sub-child-2' ) }
title="Sub-Child 2"
/>
</NavigationMenu>
</Navigation>

<div className="navigation-story__aside">
<p>
Menu <code>{ activeMenu }</code> is active.
<br />
Item <code>{ activeItem }</code> is active.
</p>
<p>
<button
onClick={ () => {
setActiveMenu( 'nested-sub-menu' );
} }
>
Open the Nested Sub-Menu menu
</button>
</p>
<p>
<button
onClick={ () => {
setActiveItem( 'child-2' );
setActiveMenu( 'sub-menu' );
} }
>
Navigate to Child 2 item
</button>
</p>
</div>
</>
);
};

describe( 'Navigation', () => {
it( 'should render the panes and active item', async () => {
render( testNavigation( { activeItem: 'item-2' } ) );
render( <TestNavigation activeItem="item-2" /> );

const menuItems = screen.getAllByRole( 'listitem' );

expect( menuItems.length ).toBe( 4 );
expect( menuItems[ 0 ].textContent ).toBe( 'Item 1' );
expect( menuItems[ 1 ].textContent ).toBe( 'Item 2' );
expect( menuItems[ 2 ].textContent ).toBe( 'Category' );
expect( menuItems[ 3 ].textContent ).toBe( 'customize me' );
expect( menuItems[ 0 ].classList.contains( 'is-active' ) ).toBe(
false
);
expect( menuItems[ 1 ].classList.contains( 'is-active' ) ).toBe( true );
expect( menuItems ).toHaveLength( 4 );
expect( menuItems[ 0 ] ).toHaveTextContent( 'Item 1' );
expect( menuItems[ 1 ] ).toHaveTextContent( 'Item 2' );
expect( menuItems[ 2 ] ).toHaveTextContent( 'Category' );
expect( menuItems[ 3 ] ).toHaveTextContent( 'customize me' );

expect(
screen.getByRole( 'link', { current: 'page' } )
).toHaveTextContent( 'Item 2' );
} );

it( 'should render anchor links when menu item supplies an href', async () => {
render( testNavigation() );
it( 'should render anchor links when menu item supplies an href', () => {
render( <TestNavigation /> );

const linkItem = screen.getByRole( 'link', { name: 'Item 2' } );

expect( linkItem ).toBeDefined();
expect( linkItem.target ).toBe( '_blank' );
expect( linkItem ).toBeInTheDocument();
expect( linkItem ).toHaveAttribute( 'target', '_blank' );
} );

it( 'should render a custom component when menu item supplies one', async () => {
render( testNavigation() );
it( 'should render a custom component when menu item supplies one', () => {
render( <TestNavigation /> );

const customItem = screen.getByText( 'customize me' );

expect( customItem ).toBeDefined();
expect( screen.getByText( 'customize me' ) ).toBeInTheDocument();
} );

it( 'should set an active category on click', async () => {
render( testNavigation() );
const user = userEvent.setup( {
advanceTimers: jest.advanceTimersByTime,
} );

fireEvent.click( screen.getByRole( 'button', { name: 'Category' } ) );
const categoryTitle = screen.getByRole( 'heading' );
const menuItems = screen.getAllByRole( 'listitem' );
render( <TestNavigation /> );

await user.click( screen.getByRole( 'button', { name: 'Category' } ) );

expect( categoryTitle.textContent ).toBe( 'Category' );
expect( menuItems.length ).toBe( 2 );
expect( menuItems[ 0 ].textContent ).toBe( 'Child 1' );
expect( menuItems[ 1 ].textContent ).toBe( 'Child 2' );
expect( screen.getByRole( 'heading' ) ).toHaveTextContent( 'Category' );
const menuItems = screen.getAllByRole( 'listitem' );
expect( menuItems ).toHaveLength( 2 );
expect( menuItems[ 0 ] ).toHaveTextContent( 'Child 1' );
expect( menuItems[ 1 ] ).toHaveTextContent( 'Child 2' );
} );

it( 'should render the root title', async () => {
const { rerender } = render( testNavigation() );
it( 'should render the root title', () => {
const { rerender } = render( <TestNavigation /> );

const emptyTitle = screen.queryByRole( 'heading' );
expect( emptyTitle ).toBeNull();
expect( screen.queryByRole( 'heading' ) ).not.toBeInTheDocument();

rerender( testNavigation( { rootTitle: 'Home' } ) );
rerender( <TestNavigation rootTitle="Home" /> );

const rootTitle = screen.getByRole( 'heading' );
expect( rootTitle.textContent ).toBe( 'Home' );
expect( screen.getByRole( 'heading' ) ).toBeInTheDocument();
expect( screen.getByRole( 'heading' ) ).toHaveTextContent( 'Home' );
} );

it( 'should render badges', async () => {
render( testNavigation( { showBadge: true } ) );
it( 'should render badges', () => {
render( <TestNavigation showBadge /> );

const menuItem = screen.getAllByRole( 'listitem' );
expect( menuItem[ 0 ].textContent ).toBe( 'Item 1' + '21' );
} );

it( 'should render menu titles when items exist', async () => {
it( 'should render menu titles when items exist', () => {
const { rerender } = render( <Navigation></Navigation> );

const emptyMenu = screen.queryByText( 'Menu title' );
expect( emptyMenu ).toBeNull();
expect( screen.queryByText( 'Menu title' ) ).not.toBeInTheDocument();

rerender( testNavigation( { rootTitle: 'Menu title' } ) );
rerender( <TestNavigation rootTitle="Menu title" /> );

const menuTitle = screen.queryByText( 'Menu title' );
expect( menuTitle ).not.toBeNull();
expect( screen.getByText( 'Menu title' ) ).toBeInTheDocument();
} );

it( 'should navigate up a level when clicking the back button', async () => {
render( testNavigation( { rootTitle: 'Home' } ) );

fireEvent.click( screen.getByRole( 'button', { name: 'Category' } ) );
let menuTitle = screen.getByRole( 'heading' );
expect( menuTitle.textContent ).toBe( 'Category' );
fireEvent.click( screen.getByRole( 'button', { name: 'Home' } ) );
menuTitle = screen.getByRole( 'heading' );
expect( menuTitle.textContent ).toBe( 'Home' );
const user = userEvent.setup( {
advanceTimers: jest.advanceTimersByTime,
} );

render( <TestNavigation rootTitle="Home" /> );

await user.click( screen.getByRole( 'button', { name: 'Category' } ) );

expect( screen.getByRole( 'heading' ) ).toHaveTextContent( 'Category' );

await user.click( screen.getByRole( 'button', { name: 'Home' } ) );

expect( screen.getByRole( 'heading' ) ).toHaveTextContent( 'Home' );
} );

it( 'should navigate correctly when controlled', async () => {
const user = userEvent.setup( {
advanceTimers: jest.advanceTimersByTime,
} );

render( <TestNavigationControlled /> );

// check root menu is shown and item 1 is selected
expect(
screen.getByRole( 'heading', { name: 'Home' } )
).toBeInTheDocument();
expect(
screen.getByRole( 'link', { current: 'page' } )
).toHaveTextContent( 'Item 1' );

// click Item 2, check it's selected
await user.click( screen.getByRole( 'link', { name: 'Item 2' } ) );
expect(
screen.getByRole( 'link', { current: 'page' } )
).toHaveTextContent( 'Item 2' );

// click sub-menu, check new menu is shown
await user.click( screen.getByRole( 'button', { name: 'Sub-Menu' } ) );
expect(
screen.getByRole( 'heading', { name: 'Sub-Menu' } )
).toBeInTheDocument();

// click Child 1, check it's selected
await user.click( screen.getByRole( 'button', { name: 'Child 1' } ) );
expect(
screen.getByRole( 'button', { current: 'page' } )
).toHaveTextContent( 'Child 1' );

// click nested sub-menu, check nested sub-menu is shown
await user.click(
screen.getByRole( 'button', { name: 'Nested Sub-Menu' } )
);
expect(
screen.getByRole( 'heading', { name: 'Nested Sub-Menu' } )
).toBeInTheDocument();

// click Sub Child 2, check it's selected
await user.click(
screen.getByRole( 'button', { name: 'Sub-Child 2' } )
);
expect(
screen.getByRole( 'button', { current: 'page' } )
).toHaveTextContent( 'Sub-Child 2' );

// click back, check sub-menu is shown
await user.click( screen.getByRole( 'button', { name: 'Sub-Menu' } ) );
expect(
screen.getByRole( 'heading', { name: 'Sub-Menu' } )
).toBeInTheDocument();

// click back, check root menu is shown
await user.click( screen.getByRole( 'button', { name: 'Home' } ) );
expect(
screen.getByRole( 'heading', { name: 'Home' } )
).toBeInTheDocument();

// click the programmatic nested sub-menu button, check nested sub menu is shown
await user.click(
screen.getByRole( 'button', {
name: 'Open the Nested Sub-Menu menu',
} )
);
expect(
screen.getByRole( 'heading', { name: 'Nested Sub-Menu' } )
).toBeInTheDocument();

// click navigate to child2 item button, check the correct menu is shown and the item is selected
await user.click(
screen.getByRole( 'button', {
name: 'Navigate to Child 2 item',
} )
);
expect(
screen.getByRole( 'heading', { name: 'Sub-Menu' } )
).toBeInTheDocument();
expect(
screen.getByRole( 'button', { current: 'page' } )
).toHaveTextContent( 'Child 2' );
} );
} );

0 comments on commit a8c8b14

Please sign in to comment.