diff --git a/packages/components/src/tab-panel/test/index.tsx b/packages/components/src/tab-panel/test/index.tsx index d2e4bd07e25a8a..a7899dd821fdde 100644 --- a/packages/components/src/tab-panel/test/index.tsx +++ b/packages/components/src/tab-panel/test/index.tsx @@ -27,6 +27,8 @@ const TABS = [ }, ]; +const getSelectedTab = () => screen.getByRole( 'tab', { selected: true } ); + let originalGetClientRects: () => DOMRectList; describe.each( [ @@ -51,4 +53,418 @@ describe.each( [ afterAll( () => { window.HTMLElement.prototype.getClientRects = originalGetClientRects; } ); + + describe( 'Without `initialTabName`', () => { + it( 'should render first tab', async () => { + const panelRenderFunction = jest.fn(); + + render( + + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Alpha' ); + expect( + screen.getByRole( 'tabpanel', { name: 'Alpha' } ) + ).toBeInTheDocument(); + expect( panelRenderFunction ).toHaveBeenLastCalledWith( TABS[ 0 ] ); + } ); + + it( 'should fall back to first enabled tab if the active tab is removed', async () => { + const mockOnSelect = jest.fn(); + const { rerender } = render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + rerender( + undefined } + onSelect={ mockOnSelect } + /> + ); + expect( getSelectedTab() ).toHaveTextContent( 'Beta' ); + } ); + } ); + + describe( 'With `initialTabName`', () => { + it( 'should render the tab set by initialTabName prop', () => { + render( + undefined } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Beta' ); + } ); + + it( 'should not render a tab when initialTabName does not exist', () => { + render( + undefined } + /> + ); + + // No tab should be rendered i.e. it doesn't fall back to first tab. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + } ); + + it( 'should not change tabs when initialTabName is changed', () => { + const { rerender } = render( + undefined } + /> + ); + + rerender( + undefined } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Beta' ); + } ); + + it( 'should fall back to initial tab if active tab is removed', async () => { + const user = userEvent.setup(); + const mockOnSelect = jest.fn(); + const { rerender } = render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + + rerender( + undefined } + onSelect={ mockOnSelect } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Gamma' ); + } ); + + it( 'waits for the tab with the `initialTabName` to be present in the `tabs` array before selecting it', () => { + const mockOnSelect = jest.fn(); + const { rerender } = render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + // There should be no selected tab yet. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + + rerender( + undefined } + onSelect={ mockOnSelect } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Delta' ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'delta' ); + } ); + } ); + + describe( 'Disabled Tab', () => { + it( 'should disable the tab when `disabled` is `true`', async () => { + const user = userEvent.setup(); + const mockOnSelect = jest.fn(); + + render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + expect( + screen.getByRole( 'tab', { name: 'Delta' } ) + ).toHaveAttribute( 'aria-disabled', 'true' ); + + // onSelect gets called on the initial render. + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + + // onSelect should not be called since the disabled tab is + // highlighted, but not selected. + await user.keyboard( '[ArrowLeft]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should select first enabled tab when initial tab is disabled', () => { + const mockOnSelect = jest.fn(); + + render( + { + if ( tab.name === 'gamma' ) { + return tab; + } + return { ...tab, disabled: true }; + } ) } + initialTabName="beta" + children={ () => undefined } + onSelect={ mockOnSelect } + /> + ); + + // As alpha (first tab), and beta (the initial tab), are both + // disabled the first enabled tab should be gamma. + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).toHaveTextContent( 'Gamma' ); + } ); + + it( 'should select the first enabled tab when the selected tab becomes disabled', () => { + const mockOnSelect = jest.fn(); + const { rerender } = render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Alpha' ); + + rerender( + { + if ( tab.name === 'alpha' ) { + return { ...tab, disabled: true }; + } + return tab; + } ) } + children={ () => undefined } + onSelect={ mockOnSelect } + /> + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Beta' ); + } ); + } ); + + describe( 'Tab Activation', () => { + it( 'should render a tabpanel, and clicking should change tabs', async () => { + const user = userEvent.setup(); + const panelRenderFunction = jest.fn(); + const mockOnSelect = jest.fn(); + + render( + + ); + + expect( getSelectedTab() ).toHaveTextContent( 'Alpha' ); + expect( + screen.getByRole( 'tabpanel', { name: 'Alpha' } ) + ).toBeInTheDocument(); + expect( panelRenderFunction ).toHaveBeenLastCalledWith( TABS[ 0 ] ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + + await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) ); + + expect( getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( + screen.getByRole( 'tabpanel', { name: 'Beta' } ) + ).toBeInTheDocument(); + expect( panelRenderFunction ).toHaveBeenLastCalledWith( TABS[ 1 ] ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + + await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + + expect( getSelectedTab() ).toHaveTextContent( 'Alpha' ); + expect( + screen.getByRole( 'tabpanel', { name: 'Alpha' } ) + ).toBeInTheDocument(); + expect( panelRenderFunction ).toHaveBeenLastCalledWith( TABS[ 0 ] ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + } ); + + it( 'defaults to automatic tab activation', async () => { + const user = userEvent.setup(); + const mockOnSelect = jest.fn(); + + render( + undefined } + onSelect={ mockOnSelect } + /> + ); + + // onSelect gets called on the initial render. + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + + // Click on Alpha. Make sure alpha is selected. + await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + + // Navigate forward with arrow keys and make sure the Beta tab is + // selected automatically. + await user.keyboard( '[ArrowRight]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + + // Navigate forward with arrow keys. Make sure gamma (last tab) is + // selected automatically. + await user.keyboard( '[ArrowRight]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); + + // Navigate forward with arrow keys. Make sure Alpha (first tab) is + // selected automatically. + await user.keyboard( '[ArrowRight]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 5 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + + // Navigate backwards with arrow keys. Make sure Gamma (last tab) is + // selected automatically. + await user.keyboard( '[ArrowLeft]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 6 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); + } ); + + it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => { + const user = userEvent.setup(); + const mockOnSelect = jest.fn(); + + render( + undefined } + onSelect={ mockOnSelect } + selectOnMove={ false } + /> + ); + + // onSelect gets called on the initial render. + expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); + + // Click on Alpha and make sure it is selected. + await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + + // Navigate forward with arrow keys. Make sure Beta is focused, but + // that the tab selection happens only when pressing the spacebar + // or enter key. + await user.keyboard( '[ArrowRight]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); + expect( screen.getByRole( 'tab', { name: 'Beta' } ) ).toHaveFocus(); + + await user.keyboard( '[Enter]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); + + // Navigate forward with arrow keys. Make sure Gamma (last tab) is + // focused, but that tab selection happens only when pressing the + // spacebar or enter key. + await user.keyboard( '[ArrowRight]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); + expect( + screen.getByRole( 'tab', { name: 'Gamma' } ) + ).toHaveFocus(); + + await user.keyboard( '[Space]' ); + expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); + expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); + + // No need to test the "wrap-around" behaviour, as it is being + // tested in the "automatic tab activation" test above. + } ); + } ); + + describe( 'Tab Attributes', () => { + it( "should apply the tab's `className` to the tab button", () => { + render( undefined } /> ); + + expect( screen.getByRole( 'tab', { name: 'Alpha' } ) ).toHaveClass( + 'alpha-class' + ); + expect( screen.getByRole( 'tab', { name: 'Beta' } ) ).toHaveClass( + 'beta-class' + ); + expect( screen.getByRole( 'tab', { name: 'Gamma' } ) ).toHaveClass( + 'gamma-class' + ); + } ); + + it( 'should apply the `activeClass` to the selected tab', async () => { + const user = userEvent.setup(); + const activeClass = 'my-active-tab'; + + render( + undefined } + /> + ); + + expect( getSelectedTab() ).toHaveClass( activeClass ); + screen + .getAllByRole( 'tab', { selected: false } ) + .forEach( ( unselectedTab ) => { + expect( unselectedTab ).not.toHaveClass( activeClass ); + } ); + + await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) ); + + expect( getSelectedTab() ).toHaveClass( activeClass ); + screen + .getAllByRole( 'tab', { selected: false } ) + .forEach( ( unselectedTab ) => { + expect( unselectedTab ).not.toHaveClass( activeClass ); + } ); + } ); + } ); } );