Skip to content

Commit

Permalink
ToolsPanel: refactor unit tests to TypeScript (#48275)
Browse files Browse the repository at this point in the history
* rename file

* Move control value from props object to separate variable

* Fix context type

* Remove disabled option in the query, replace with aria-disabled attribute check

* Reuse ToolsPanelItem component props type

* Avoid reusing the same variable to hold two different query results

* Type component

* Add missing required props to ToolsPanelItem

* Add types to internal `ToolsPanelOptional` component

* Prefer React global for types

* CHANGELOG

* Extract controlvalue props
  • Loading branch information
ciampo committed Feb 21, 2023
1 parent cfb54a7 commit a9db335
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 61 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- `ToolsPanel`: Ensure display of optional items when values are updated externally and multiple blocks selected ([47864](https://github.com/WordPress/gutenberg/pull/47864)).
- `Navigator`: add more pattern matching tests, refine existing tests ([47910](https://github.com/WordPress/gutenberg/pull/47910)).
- `ToolsPanel`: Refactor Storybook examples to TypeScript ([47944](https://github.com/WordPress/gutenberg/pull/47944)).
- `ToolsPanel`: Refactor unit tests to TypeScript ([48275](https://github.com/WordPress/gutenberg/pull/48275)).

## 23.3.0 (2023-02-01)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,64 @@ import userEvent from '@testing-library/user-event';
*/
import { ToolsPanel, ToolsPanelContext, ToolsPanelItem } from '../';
import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill';
import type { ToolsPanelContext as ToolsPanelContextType } from '../types';

const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' );
const resetAll = jest.fn();
const noop = () => undefined;

type ControlValue = boolean | undefined;

// Default props for the tools panel.
const defaultProps = {
label: 'Panel header',
resetAll,
};

// Default props for an enabled control to be rendered within panel.
let controlValue: ControlValue = true;
const controlProps = {
attributes: { value: true },
hasValue: jest.fn().mockImplementation( () => {
return !! controlProps.attributes.value;
return !! controlValue;
} ),
label: 'Example',
onDeselect: jest.fn().mockImplementation( () => {
controlProps.attributes.value = undefined;
controlValue = undefined;
} ),
onSelect: jest.fn(),
};

// Default props without a value for an alternate control to be rendered within
// the panel.
let altControlValue: ControlValue = false;
const altControlProps = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! altControlProps.attributes.value;
return !! altControlValue;
} ),
label: 'Alt',
onDeselect: jest.fn(),
onSelect: jest.fn(),
};

// Default props for wrapped or grouped panel items.
let nestedControlValue: ControlValue = true;
const nestedControlProps = {
attributes: { value: true },
hasValue: jest.fn().mockImplementation( () => {
return !! nestedControlProps.attributes.value;
return !! nestedControlValue;
} ),
label: 'Nested Control 1',
onDeselect: jest.fn().mockImplementation( () => {
nestedControlProps.attributes.value = undefined;
nestedControlValue = undefined;
} ),
onSelect: jest.fn(),
isShownByDefault: true,
};

// Alternative props for wrapped or grouped panel items.
const altNestedControlValue: ControlValue = false;
const altNestedControlProps = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! altNestedControlProps.attributes.value;
return !! altNestedControlValue;
} ),
label: 'Nested Control 2',
onDeselect: jest.fn(),
Expand All @@ -90,7 +93,7 @@ const GroupedItems = ( {

// This context object is used to help simulate different scenarios in which
// `ToolsPanelItem` registration or deregistration requires testing.
const panelContext = {
const panelContext: ToolsPanelContextType = {
panelId: '1234',
menuItems: {
default: {},
Expand All @@ -117,7 +120,10 @@ const renderGroupedItemsInPanel = () => {

// Custom component rendering a panel item within a wrapping element. Also used
// to test panel item registration and rendering.
const WrappedItem = ( { text, ...props } ) => {
const WrappedItem = ( {
text,
...props
}: React.ComponentProps< typeof ToolsPanelItem > & { text: string } ) => {
return (
<div>
<span>Wrapper</span>
Expand Down Expand Up @@ -178,16 +184,16 @@ const openDropdownMenu = async () => {
};

// Opens dropdown then selects the menu item by label before simulating a click.
const selectMenuItem = async ( label ) => {
const selectMenuItem = async ( label: string ) => {
const user = userEvent.setup();
const menuItem = await screen.findByText( label );
await user.click( menuItem );
};

describe( 'ToolsPanel', () => {
afterEach( () => {
controlProps.attributes.value = true;
altControlProps.attributes.value = false;
controlValue = true;
altControlValue = false;
} );

describe( 'basic rendering', () => {
Expand Down Expand Up @@ -229,10 +235,20 @@ describe( 'ToolsPanel', () => {
render(
<ToolsPanel { ...defaultProps }>
{ false && (
<ToolsPanelItem>Should not show</ToolsPanelItem>
<ToolsPanelItem
label="Not rendered 1"
hasValue={ () => false }
>
Should not show
</ToolsPanelItem>
) }
{ false && (
<ToolsPanelItem>Not shown either</ToolsPanelItem>
<ToolsPanelItem
label="Not rendered 2"
hasValue={ () => false }
>
Not shown either
</ToolsPanelItem>
) }
<span>Visible but insignificant</span>
</ToolsPanel>
Expand Down Expand Up @@ -317,7 +333,11 @@ describe( 'ToolsPanel', () => {
} );

it( 'should render optional panel item when value is updated externally and panel has an ID', async () => {
const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
const ToolsPanelOptional = ( {
toolsPanelItemValue,
}: {
toolsPanelItemValue?: number;
} ) => {
const itemProps = {
attributes: { value: toolsPanelItemValue },
hasValue: () => !! toolsPanelItemValue,
Expand Down Expand Up @@ -349,7 +369,11 @@ describe( 'ToolsPanel', () => {

it( 'should render optional item when value is updated externally and panelId is null', async () => {
// This test partially covers: https://github.com/WordPress/gutenberg/issues/47368
const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
const ToolsPanelOptional = ( {
toolsPanelItemValue,
}: {
toolsPanelItemValue?: number;
} ) => {
const itemProps = {
attributes: { value: toolsPanelItemValue },
hasValue: () => !! toolsPanelItemValue,
Expand Down Expand Up @@ -452,10 +476,10 @@ describe( 'ToolsPanel', () => {
} );

it( 'should render default controls with conditional isShownByDefault', async () => {
const linkedControlValue = false;
const linkedControlProps = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! linkedControlProps.attributes.value;
return !! linkedControlValue;
} ),
label: 'Linked',
onDeselect: jest.fn(),
Expand All @@ -472,7 +496,7 @@ describe( 'ToolsPanel', () => {
</ToolsPanelItem>
<ToolsPanelItem
{ ...linkedControlProps }
isShownByDefault={ !! altControlProps.attributes.value }
isShownByDefault={ !! altControlValue }
>
<div>Linked control</div>
</ToolsPanelItem>
Expand All @@ -495,13 +519,14 @@ describe( 'ToolsPanel', () => {
expect( menuGroups.length ).toEqual( 3 );

// The linked control should be in the second group, of optional controls.
let optionalItem = within( menuGroups[ 1 ] ).getByText( 'Linked' );
expect( optionalItem ).toBeInTheDocument();
expect(
within( menuGroups[ 1 ] ).getByText( 'Linked' )
).toBeInTheDocument();

// Simulate the main control having a value set which should
// trigger the linked control becoming a default control via the
// conditional `isShownByDefault` prop.
altControlProps.attributes.value = true;
altControlValue = true;

rerender( <TestPanel /> );

Expand All @@ -526,17 +551,18 @@ describe( 'ToolsPanel', () => {
// Optional controls have an additional aria-label. This can be used
// to confirm the conditional default control has been removed from
// the optional menu item group.
optionalItem = screen.queryByRole( 'menuitemcheckbox', {
name: 'Show Linked',
} );
expect( optionalItem ).not.toBeInTheDocument();
expect(
screen.queryByRole( 'menuitemcheckbox', {
name: 'Show Linked',
} )
).not.toBeInTheDocument();
} );

it( 'should handle conditionally rendered default control', async () => {
const conditionalControlValue = false;
const conditionalControlProps = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! conditionalControlProps.attributes.value;
return !! conditionalControlValue;
} ),
label: 'Conditional',
onDeselect: jest.fn(),
Expand All @@ -551,7 +577,7 @@ describe( 'ToolsPanel', () => {
>
<div>Default control</div>
</ToolsPanelItem>
{ !! altControlProps.attributes.value && (
{ !! altControlValue && (
<ToolsPanelItem
{ ...conditionalControlProps }
isShownByDefault={ true }
Expand Down Expand Up @@ -579,7 +605,7 @@ describe( 'ToolsPanel', () => {

// Simulate the main control having a value set which will now
// render the new default control into the ToolsPanel.
altControlProps.attributes.value = true;
altControlValue = true;

rerender( <TestPanel /> );

Expand Down Expand Up @@ -614,7 +640,7 @@ describe( 'ToolsPanel', () => {
// themselves, while those for the old panelId deregister.
//
// See: https://github.com/WordPress/gutenberg/pull/36588
const context = { ...panelContext };
const context: ToolsPanelContextType = { ...panelContext };
const TestPanel = () => (
<ToolsPanelContext.Provider value={ context }>
<ToolsPanelItem { ...altControlProps } panelId="1234">
Expand Down Expand Up @@ -678,7 +704,10 @@ describe( 'ToolsPanel', () => {
// individual items should still render themselves in this case.
//
// See: https://github.com/WordPress/gutenberg/pull/37216
const context = { ...panelContext, panelId: null };
const context: ToolsPanelContextType = {
...panelContext,
panelId: null,
};
const TestPanel = () => (
<ToolsPanelContext.Provider value={ context }>
<ToolsPanelItem { ...altControlProps } panelId="1234">
Expand Down Expand Up @@ -981,7 +1010,12 @@ describe( 'ToolsPanel', () => {
// test that no orphaned items appear registered in the panel menu.
//
// See: https://github.com/WordPress/gutenberg/pull/34085
const TestSlotFillPanel = ( { panelId } ) => (
const TestSlotFillPanel = ( {
panelId,
}: Pick<
React.ComponentProps< typeof ToolsPanelItem >,
'panelId'
> ) => (
<SlotFillProvider>
<ToolsPanelItems>
<ToolsPanelItem { ...altControlProps } panelId="1234">
Expand All @@ -1004,48 +1038,49 @@ describe( 'ToolsPanel', () => {

// Only the item matching the panelId should have been registered
// and appear in the panel menu.
let altMenuItem = screen.getByRole( 'menuitemcheckbox', {
name: 'Show Alt',
} );
let exampleMenuItem = screen.queryByRole( 'menuitemcheckbox', {
name: 'Hide and reset Example',
} );

expect( altMenuItem ).toBeInTheDocument();
expect( exampleMenuItem ).not.toBeInTheDocument();
expect(
screen.getByRole( 'menuitemcheckbox', {
name: 'Show Alt',
} )
).toBeInTheDocument();
expect(
screen.queryByRole( 'menuitemcheckbox', {
name: 'Hide and reset Example',
} )
).not.toBeInTheDocument();

// Re-render the panel with different panelID simulating a block
// selection change.
rerender( <TestSlotFillPanel panelId="9999" /> );

altMenuItem = screen.queryByRole( 'menuitemcheckbox', {
name: 'Show Alt',
} );
exampleMenuItem = screen.getByRole( 'menuitemcheckbox', {
name: 'Hide and reset Example',
} );

expect( altMenuItem ).not.toBeInTheDocument();
expect( exampleMenuItem ).toBeInTheDocument();
expect(
screen.queryByRole( 'menuitemcheckbox', {
name: 'Show Alt',
} )
).not.toBeInTheDocument();
expect(
screen.getByRole( 'menuitemcheckbox', {
name: 'Hide and reset Example',
} )
).toBeInTheDocument();
} );
} );

describe( 'panel header icon toggle', () => {
const defaultControlsValue = false;
const defaultControls = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! defaultControls.attributes.value;
return !! defaultControlsValue;
} ),
label: 'Default',
onDeselect: jest.fn(),
onSelect: jest.fn(),
isShownByDefault: true,
};

const optionalControlsValue = false;
const optionalControls = {
attributes: { value: false },
hasValue: jest.fn().mockImplementation( () => {
return !! optionalControls.attributes.value;
return !! optionalControlsValue;
} ),
label: 'Optional',
onDeselect: jest.fn(),
Expand Down Expand Up @@ -1115,9 +1150,10 @@ describe( 'ToolsPanel', () => {
await openDropdownMenu();

const resetAllItem = await screen.findByRole( 'menuitem', {
disabled: false,
name: 'Reset all',
} );
expect( resetAllItem ).toBeInTheDocument();
expect( resetAllItem ).toHaveAttribute( 'aria-disabled', 'false' );

await selectMenuItem( 'Reset all' );

Expand All @@ -1126,9 +1162,13 @@ describe( 'ToolsPanel', () => {
expect( announcement ).toHaveAttribute( 'aria-live', 'assertive' );

const disabledResetAllItem = await screen.findByRole( 'menuitem', {
disabled: true,
name: 'Reset all',
} );
expect( disabledResetAllItem ).toBeInTheDocument();
expect( disabledResetAllItem ).toHaveAttribute(
'aria-disabled',
'true'
);
} );
} );

Expand Down

1 comment on commit a9db335

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in a9db335.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4235654219
📝 Reported issues:

Please sign in to comment.