Skip to content

Commit a9db335

Browse files
authored
ToolsPanel: refactor unit tests to TypeScript (#48275)
* 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
1 parent cfb54a7 commit a9db335

File tree

2 files changed

+102
-61
lines changed

2 files changed

+102
-61
lines changed

‎packages/components/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- `ToolsPanel`: Ensure display of optional items when values are updated externally and multiple blocks selected ([47864](https://.com/WordPress/gutenberg/pull/47864)).
3535
- `Navigator`: add more pattern matching tests, refine existing tests ([47910](https://.com/WordPress/gutenberg/pull/47910)).
3636
- `ToolsPanel`: Refactor Storybook examples to TypeScript ([47944](https://.com/WordPress/gutenberg/pull/47944)).
37+
- `ToolsPanel`: Refactor unit tests to TypeScript ([48275](https://.com/WordPress/gutenberg/pull/48275)).
3738

3839
## 23.3.0 (2023-02-01)
3940

‎packages/components/src/tools-panel/test/index.js renamed to ‎packages/components/src/tools-panel/test/index.tsx

+101-61
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,64 @@ import userEvent from '@testing-library/user-event';
99
*/
1010
import { ToolsPanel, ToolsPanelContext, ToolsPanelItem } from '../';
1111
import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill';
12+
import type { ToolsPanelContext as ToolsPanelContextType } from '../types';
1213

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

18+
type ControlValue = boolean | undefined;
19+
1720
// Default props for the tools panel.
1821
const defaultProps = {
1922
label: 'Panel header',
2023
resetAll,
2124
};
2225

2326
// Default props for an enabled control to be rendered within panel.
27+
let controlValue: ControlValue = true;
2428
const controlProps = {
25-
attributes: { value: true },
2629
hasValue: jest.fn().mockImplementation( () => {
27-
return !! controlProps.attributes.value;
30+
return !! controlValue;
2831
} ),
2932
label: 'Example',
3033
onDeselect: jest.fn().mockImplementation( () => {
31-
controlProps.attributes.value = undefined;
34+
controlValue = undefined;
3235
} ),
3336
onSelect: jest.fn(),
3437
};
3538

3639
// Default props without a value for an alternate control to be rendered within
3740
// the panel.
41+
let altControlValue: ControlValue = false;
3842
const altControlProps = {
39-
attributes: { value: false },
4043
hasValue: jest.fn().mockImplementation( () => {
41-
return !! altControlProps.attributes.value;
44+
return !! altControlValue;
4245
} ),
4346
label: 'Alt',
4447
onDeselect: jest.fn(),
4548
onSelect: jest.fn(),
4649
};
4750

4851
// Default props for wrapped or grouped panel items.
52+
let nestedControlValue: ControlValue = true;
4953
const nestedControlProps = {
50-
attributes: { value: true },
5154
hasValue: jest.fn().mockImplementation( () => {
52-
return !! nestedControlProps.attributes.value;
55+
return !! nestedControlValue;
5356
} ),
5457
label: 'Nested Control 1',
5558
onDeselect: jest.fn().mockImplementation( () => {
56-
nestedControlProps.attributes.value = undefined;
59+
nestedControlValue = undefined;
5760
} ),
5861
onSelect: jest.fn(),
5962
isShownByDefault: true,
6063
};
6164

6265
// Alternative props for wrapped or grouped panel items.
66+
const altNestedControlValue: ControlValue = false;
6367
const altNestedControlProps = {
64-
attributes: { value: false },
6568
hasValue: jest.fn().mockImplementation( () => {
66-
return !! altNestedControlProps.attributes.value;
69+
return !! altNestedControlValue;
6770
} ),
6871
label: 'Nested Control 2',
6972
onDeselect: jest.fn(),
@@ -90,7 +93,7 @@ const GroupedItems = ( {
9093

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

118121
// Custom component rendering a panel item within a wrapping element. Also used
119122
// to test panel item registration and rendering.
120-
const WrappedItem = ( { text, ...props } ) => {
123+
const WrappedItem = ( {
124+
text,
125+
...props
126+
}: React.ComponentProps< typeof ToolsPanelItem > & { text: string } ) => {
121127
return (
122128
<div>
123129
<span>Wrapper</span>
@@ -178,16 +184,16 @@ const openDropdownMenu = async () => {
178184
};
179185

180186
// Opens dropdown then selects the menu item by label before simulating a click.
181-
const selectMenuItem = async ( label ) => {
187+
const selectMenuItem = async ( label: string ) => {
182188
const user = userEvent.setup();
183189
const menuItem = await screen.findByText( label );
184190
await user.click( menuItem );
185191
};
186192

187193
describe( 'ToolsPanel', () => {
188194
afterEach( () => {
189-
controlProps.attributes.value = true;
190-
altControlProps.attributes.value = false;
195+
controlValue = true;
196+
altControlValue = false;
191197
} );
192198

193199
describe( 'basic rendering', () => {
@@ -229,10 +235,20 @@ describe( 'ToolsPanel', () => {
229235
render(
230236
<ToolsPanel { ...defaultProps }>
231237
{ false && (
232-
<ToolsPanelItem>Should not show</ToolsPanelItem>
238+
<ToolsPanelItem
239+
label="Not rendered 1"
240+
hasValue={ () => false }
241+
>
242+
Should not show
243+
</ToolsPanelItem>
233244
) }
234245
{ false && (
235-
<ToolsPanelItem>Not shown either</ToolsPanelItem>
246+
<ToolsPanelItem
247+
label="Not rendered 2"
248+
hasValue={ () => false }
249+
>
250+
Not shown either
251+
</ToolsPanelItem>
236252
) }
237253
<span>Visible but insignificant</span>
238254
</ToolsPanel>
@@ -317,7 +333,11 @@ describe( 'ToolsPanel', () => {
317333
} );
318334

319335
it( 'should render optional panel item when value is updated externally and panel has an ID', async () => {
320-
const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
336+
const ToolsPanelOptional = ( {
337+
toolsPanelItemValue,
338+
}: {
339+
toolsPanelItemValue?: number;
340+
} ) => {
321341
const itemProps = {
322342
attributes: { value: toolsPanelItemValue },
323343
hasValue: () => !! toolsPanelItemValue,
@@ -349,7 +369,11 @@ describe( 'ToolsPanel', () => {
349369

350370
it( 'should render optional item when value is updated externally and panelId is null', async () => {
351371
// This test partially covers: https://.com/WordPress/gutenberg/issues/47368
352-
const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
372+
const ToolsPanelOptional = ( {
373+
toolsPanelItemValue,
374+
}: {
375+
toolsPanelItemValue?: number;
376+
} ) => {
353377
const itemProps = {
354378
attributes: { value: toolsPanelItemValue },
355379
hasValue: () => !! toolsPanelItemValue,
@@ -452,10 +476,10 @@ describe( 'ToolsPanel', () => {
452476
} );
453477

454478
it( 'should render default controls with conditional isShownByDefault', async () => {
479+
const linkedControlValue = false;
455480
const linkedControlProps = {
456-
attributes: { value: false },
457481
hasValue: jest.fn().mockImplementation( () => {
458-
return !! linkedControlProps.attributes.value;
482+
return !! linkedControlValue;
459483
} ),
460484
label: 'Linked',
461485
onDeselect: jest.fn(),
@@ -472,7 +496,7 @@ describe( 'ToolsPanel', () => {
472496
</ToolsPanelItem>
473497
<ToolsPanelItem
474498
{ ...linkedControlProps }
475-
isShownByDefault={ !! altControlProps.attributes.value }
499+
isShownByDefault={ !! altControlValue }
476500
>
477501
<div>Linked control</div>
478502
</ToolsPanelItem>
@@ -495,13 +519,14 @@ describe( 'ToolsPanel', () => {
495519
expect( menuGroups.length ).toEqual( 3 );
496520

497521
// The linked control should be in the second group, of optional controls.
498-
let optionalItem = within( menuGroups[ 1 ] ).getByText( 'Linked' );
499-
expect( optionalItem ).toBeInTheDocument();
522+
expect(
523+
within( menuGroups[ 1 ] ).getByText( 'Linked' )
524+
).toBeInTheDocument();
500525

501526
// Simulate the main control having a value set which should
502527
// trigger the linked control becoming a default control via the
503528
// conditional `isShownByDefault` prop.
504-
altControlProps.attributes.value = true;
529+
altControlValue = true;
505530

506531
rerender( <TestPanel /> );
507532

@@ -526,17 +551,18 @@ describe( 'ToolsPanel', () => {
526551
// Optional controls have an additional aria-label. This can be used
527552
// to confirm the conditional default control has been removed from
528553
// the optional menu item group.
529-
optionalItem = screen.queryByRole( 'menuitemcheckbox', {
530-
name: 'Show Linked',
531-
} );
532-
expect( optionalItem ).not.toBeInTheDocument();
554+
expect(
555+
screen.queryByRole( 'menuitemcheckbox', {
556+
name: 'Show Linked',
557+
} )
558+
).not.toBeInTheDocument();
533559
} );
534560

535561
it( 'should handle conditionally rendered default control', async () => {
562+
const conditionalControlValue = false;
536563
const conditionalControlProps = {
537-
attributes: { value: false },
538564
hasValue: jest.fn().mockImplementation( () => {
539-
return !! conditionalControlProps.attributes.value;
565+
return !! conditionalControlValue;
540566
} ),
541567
label: 'Conditional',
542568
onDeselect: jest.fn(),
@@ -551,7 +577,7 @@ describe( 'ToolsPanel', () => {
551577
>
552578
<div>Default control</div>
553579
</ToolsPanelItem>
554-
{ !! altControlProps.attributes.value && (
580+
{ !! altControlValue && (
555581
<ToolsPanelItem
556582
{ ...conditionalControlProps }
557583
isShownByDefault={ true }
@@ -579,7 +605,7 @@ describe( 'ToolsPanel', () => {
579605

580606
// Simulate the main control having a value set which will now
581607
// render the new default control into the ToolsPanel.
582-
altControlProps.attributes.value = true;
608+
altControlValue = true;
583609

584610
rerender( <TestPanel /> );
585611

@@ -614,7 +640,7 @@ describe( 'ToolsPanel', () => {
614640
// themselves, while those for the old panelId deregister.
615641
//
616642
// See: https://.com/WordPress/gutenberg/pull/36588
617-
const context = { ...panelContext };
643+
const context: ToolsPanelContextType = { ...panelContext };
618644
const TestPanel = () => (
619645
<ToolsPanelContext.Provider value={ context }>
620646
<ToolsPanelItem { ...altControlProps } panelId="1234">
@@ -678,7 +704,10 @@ describe( 'ToolsPanel', () => {
678704
// individual items should still render themselves in this case.
679705
//
680706
// See: https://.com/WordPress/gutenberg/pull/37216
681-
const context = { ...panelContext, panelId: null };
707+
const context: ToolsPanelContextType = {
708+
...panelContext,
709+
panelId: null,
710+
};
682711
const TestPanel = () => (
683712
<ToolsPanelContext.Provider value={ context }>
684713
<ToolsPanelItem { ...altControlProps } panelId="1234">
@@ -981,7 +1010,12 @@ describe( 'ToolsPanel', () => {
9811010
// test that no orphaned items appear registered in the panel menu.
9821011
//
9831012
// See: https://.com/WordPress/gutenberg/pull/34085
984-
const TestSlotFillPanel = ( { panelId } ) => (
1013+
const TestSlotFillPanel = ( {
1014+
panelId,
1015+
}: Pick<
1016+
React.ComponentProps< typeof ToolsPanelItem >,
1017+
'panelId'
1018+
> ) => (
9851019
<SlotFillProvider>
9861020
<ToolsPanelItems>
9871021
<ToolsPanelItem { ...altControlProps } panelId="1234">
@@ -1004,48 +1038,49 @@ describe( 'ToolsPanel', () => {
10041038

10051039
// Only the item matching the panelId should have been registered
10061040
// and appear in the panel menu.
1007-
let altMenuItem = screen.getByRole( 'menuitemcheckbox', {
1008-
name: 'Show Alt',
1009-
} );
1010-
let exampleMenuItem = screen.queryByRole( 'menuitemcheckbox', {
1011-
name: 'Hide and reset Example',
1012-
} );
1013-
1014-
expect( altMenuItem ).toBeInTheDocument();
1015-
expect( exampleMenuItem ).not.toBeInTheDocument();
1041+
expect(
1042+
screen.getByRole( 'menuitemcheckbox', {
1043+
name: 'Show Alt',
1044+
} )
1045+
).toBeInTheDocument();
1046+
expect(
1047+
screen.queryByRole( 'menuitemcheckbox', {
1048+
name: 'Hide and reset Example',
1049+
} )
1050+
).not.toBeInTheDocument();
10161051

10171052
// Re-render the panel with different panelID simulating a block
10181053
// selection change.
10191054
rerender( <TestSlotFillPanel panelId="9999" /> );
1020-
1021-
altMenuItem = screen.queryByRole( 'menuitemcheckbox', {
1022-
name: 'Show Alt',
1023-
} );
1024-
exampleMenuItem = screen.getByRole( 'menuitemcheckbox', {
1025-
name: 'Hide and reset Example',
1026-
} );
1027-
1028-
expect( altMenuItem ).not.toBeInTheDocument();
1029-
expect( exampleMenuItem ).toBeInTheDocument();
1055+
expect(
1056+
screen.queryByRole( 'menuitemcheckbox', {
1057+
name: 'Show Alt',
1058+
} )
1059+
).not.toBeInTheDocument();
1060+
expect(
1061+
screen.getByRole( 'menuitemcheckbox', {
1062+
name: 'Hide and reset Example',
1063+
} )
1064+
).toBeInTheDocument();
10301065
} );
10311066
} );
10321067

10331068
describe( 'panel header icon toggle', () => {
1069+
const defaultControlsValue = false;
10341070
const defaultControls = {
1035-
attributes: { value: false },
10361071
hasValue: jest.fn().mockImplementation( () => {
1037-
return !! defaultControls.attributes.value;
1072+
return !! defaultControlsValue;
10381073
} ),
10391074
label: 'Default',
10401075
onDeselect: jest.fn(),
10411076
onSelect: jest.fn(),
10421077
isShownByDefault: true,
10431078
};
10441079

1080+
const optionalControlsValue = false;
10451081
const optionalControls = {
1046-
attributes: { value: false },
10471082
hasValue: jest.fn().mockImplementation( () => {
1048-
return !! optionalControls.attributes.value;
1083+
return !! optionalControlsValue;
10491084
} ),
10501085
label: 'Optional',
10511086
onDeselect: jest.fn(),
@@ -1115,9 +1150,10 @@ describe( 'ToolsPanel', () => {
11151150
await openDropdownMenu();
11161151

11171152
const resetAllItem = await screen.findByRole( 'menuitem', {
1118-
disabled: false,
1153+
name: 'Reset all',
11191154
} );
11201155
expect( resetAllItem ).toBeInTheDocument();
1156+
expect( resetAllItem ).toHaveAttribute( 'aria-disabled', 'false' );
11211157

11221158
await selectMenuItem( 'Reset all' );
11231159

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

11281164
const disabledResetAllItem = await screen.findByRole( 'menuitem', {
1129-
disabled: true,
1165+
name: 'Reset all',
11301166
} );
11311167
expect( disabledResetAllItem ).toBeInTheDocument();
1168+
expect( disabledResetAllItem ).toHaveAttribute(
1169+
'aria-disabled',
1170+
'true'
1171+
);
11321172
} );
11331173
} );
11341174

0 commit comments

Comments
 (0)