diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 26173ee53bce7c..e5290611a93e2c 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -52,6 +52,7 @@
- `CustomGradientBar`: Refactor away from Lodash ([#45367](https://github.com/WordPress/gutenberg/pull/45367/)).
- `TextControl`: Set Storybook control types on `help`, `label` and `type` ([#45405](https://github.com/WordPress/gutenberg/pull/45405)).
- `Autocomplete`: use Popover's new `placement` prop instead of legacy `position` prop ([#44396](https://github.com/WordPress/gutenberg/pull/44396/)).
+- `FontSizePicker`: Add more comprehensive tests ([#45298](https://github.com/WordPress/gutenberg/pull/45298)).
## 21.3.0 (2022-10-19)
diff --git a/packages/components/src/font-size-picker/test/index.tsx b/packages/components/src/font-size-picker/test/index.tsx
index 719dd612c45d95..25a21da92527e0 100644
--- a/packages/components/src/font-size-picker/test/index.tsx
+++ b/packages/components/src/font-size-picker/test/index.tsx
@@ -1,290 +1,522 @@
/**
* External dependencies
*/
-import { render, fireEvent, screen } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import FontSizePicker from '../';
-
-const getUnitSelect = () =>
- document.body.querySelector( '.components-unit-control select' );
-const getUnitLabel = () =>
- document.body.querySelector( '.components-unit-control__unit-label' );
-
-const toggleCustomInput = ( showCustomInput: boolean ) => {
- const label = showCustomInput ? 'Set custom size' : 'Use size preset';
- const toggleCustom = screen.getByLabelText( label, { selector: 'button' } );
- fireEvent.click( toggleCustom );
-};
+import type { FontSize } from '../types';
describe( 'FontSizePicker', () => {
- describe( 'onChange values', () => {
- it( 'should not use units when the initial value is a number', () => {
- let fontSize = 12;
- const setFontSize = jest.fn(
- ( nextSize ) => ( fontSize = nextSize )
- );
-
+ test.each( [
+ // Use units when initial value uses units.
+ { value: '12px', expectedValue: '80px' },
+ // Don't use units when initial value does not use units.
+ { value: 12, expectedValue: 80 },
+ ] )(
+ 'should call onChange( $expectedValue ) after user types 80 when value is $value',
+ async ( { value, expectedValue } ) => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
render(
);
+ const input = screen.getByLabelText( 'Custom' );
+ await user.clear( input );
+ await user.type( input, '80' );
+ expect( onChange ).toHaveBeenCalledTimes( 3 ); // Once for the clear, then once per keystroke.
+ expect( onChange ).toHaveBeenCalledWith( expectedValue );
+ }
+ );
- const unitSelect = getUnitSelect();
- const unitLabel = getUnitLabel();
- const input = screen.getByLabelText( 'Custom', {
- selector: 'input',
+ test.each( [
+ // Use units when first font size uses units.
+ { firstFontSize: '12px', expectedValue: '80px' },
+ // Don't use units when first font size does not use units.
+ { firstFontSize: 12, expectedValue: 80 },
+ ] )(
+ 'should call onChange( $expectedValue ) after user types 80 when first font size is $firstFontSize',
+ async ( { firstFontSize, expectedValue } ) => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
} );
-
- input.focus();
- fireEvent.change( input, { target: { value: 16 } } );
-
- expect( unitSelect ).toBeFalsy();
- expect( unitLabel ).toBeTruthy();
- expect( fontSize ).toBe( 16 );
- } );
-
- it( 'should use units when the initial value has a unit', () => {
- let fontSize = '12px';
- const setFontSize = jest.fn(
- ( nextSize ) => ( fontSize = nextSize )
- );
-
+ const onChange = jest.fn();
render(
);
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ const input = screen.getByLabelText( 'Custom' );
+ await user.type( input, '80' );
+ expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke.
+ expect( onChange ).toHaveBeenCalledWith( expectedValue );
+ }
+ );
- const unitSelect = getUnitSelect();
- const unitLabel = getUnitLabel();
- const input = screen.getByLabelText( 'Custom', {
- selector: 'input',
- } );
-
- input.focus();
- fireEvent.change( input, { target: { value: 16 } } );
+ describe( 'with > 5 font sizes', () => {
+ const fontSizes = [
+ {
+ slug: 'tiny',
+ name: 'Tiny',
+ size: '8px',
+ },
+ {
+ slug: 'small',
+ name: 'Small',
+ size: '1em',
+ },
+ {
+ slug: 'medium',
+ name: 'Medium',
+ size: '2rem',
+ },
+ {
+ slug: 'large',
+ name: 'Large',
+ size: 'clamp(1.75rem, 3vw, 2.25rem)',
+ },
+ {
+ slug: 'x-large',
+ name: 'Extra Large',
+ size: '30px',
+ },
+ {
+ slug: 'xx-large',
+ // no name
+ size: '40px',
+ },
+ ];
- expect( unitSelect ).toBeTruthy();
- expect( unitLabel ).toBeFalsy();
- expect( fontSize ).toBe( '16px' );
+ it( 'displays a select control', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Font size' } )
+ );
+ const options = screen.getAllByRole( 'option' );
+ expect( options ).toHaveLength( 8 );
+ expect( options[ 0 ] ).toHaveAccessibleName( 'Default' );
+ expect( options[ 1 ] ).toHaveAccessibleName( 'Tiny 8' );
+ expect( options[ 2 ] ).toHaveAccessibleName( 'Small 1' );
+ expect( options[ 3 ] ).toHaveAccessibleName( 'Medium 2' );
+ expect( options[ 4 ] ).toHaveAccessibleName( 'Large' );
+ expect( options[ 5 ] ).toHaveAccessibleName( 'Extra Large 30' );
+ expect( options[ 6 ] ).toHaveAccessibleName( 'xx-large 40' );
+ expect( options[ 7 ] ).toHaveAccessibleName( 'Custom' );
} );
- it( 'should not use units when fontSizes size values are numbers', () => {
- let fontSize;
- const fontSizes = [
- {
- name: 'Small',
- slug: 'small',
- size: 12,
- },
- ];
- const setFontSize = jest.fn(
- ( nextSize ) => ( fontSize = nextSize )
- );
+ test.each( [
+ { value: undefined, expectedLabel: 'Size' },
+ { value: '8px', expectedLabel: 'Size (8px)' },
+ { value: '1em', expectedLabel: 'Size (1em)' },
+ { value: '2rem', expectedLabel: 'Size (2rem)' },
+ { value: 'clamp(1.75rem, 3vw, 2.25rem)', expectedLabel: 'Size' },
+ { value: '3px', expectedLabel: 'Size (Custom)' },
+ ] )(
+ 'displays $expectedLabel as label when value is $value',
+ ( { value, expectedLabel } ) => {
+ render(
+
+ );
+ expect(
+ screen.getByLabelText( expectedLabel )
+ ).toBeInTheDocument();
+ }
+ );
+ test.each( [
+ { option: 'Default', value: '8px', expectedValue: undefined },
+ { option: 'Tiny 8', value: undefined, expectedValue: '8px' },
+ { option: 'Small 1', value: '8px', expectedValue: '1em' },
+ { option: 'Medium 2', value: '8px', expectedValue: '2rem' },
+ {
+ option: 'Large',
+ value: '8px',
+ expectedValue: 'clamp(1.75rem, 3vw, 2.25rem)',
+ },
+ ] )(
+ 'calls onChange( $expectedValue ) when $option is selected',
+ async ( { option, value, expectedValue } ) => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Font size' } )
+ );
+ await user.click(
+ screen.getByRole( 'option', { name: option } )
+ );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( expectedValue );
+ }
+ );
+
+ it( 'shows custom input when Custom is selected', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
render(
);
+ await user.click(
+ screen.getByRole( 'button', { name: 'Font size' } )
+ );
+ await user.click(
+ screen.getByRole( 'option', { name: 'Custom' } )
+ );
+ expect( screen.getByLabelText( 'Custom' ) ).toBeInTheDocument();
+ // TODO: onChange() shouldn't be called.
+ //expect( onChange ).not.toHaveBeenCalled();
+ } );
- toggleCustomInput( true );
- const unitSelect = getUnitSelect();
- const unitLabel = getUnitLabel();
- const input = screen.getByLabelText( 'Custom', {
- selector: 'input',
- } );
+ commonTests( fontSizes );
+ } );
- input.focus();
- fireEvent.change( input, { target: { value: 16 } } );
+ describe( 'with ≤ 5 homogeneous font sizes', () => {
+ const fontSizes = [
+ {
+ slug: 'small',
+ // no name
+ size: '12px',
+ },
+ {
+ slug: 'medium',
+ name: 'Medium',
+ size: '16px',
+ },
+ {
+ slug: 'large',
+ name: 'Large',
+ size: '22px',
+ },
+ {
+ slug: 'huge',
+ name: 'Huge',
+ size: '30px',
+ },
+ {
+ slug: 'gigantosaurus',
+ name: 'Gigantosaurus',
+ size: '40px',
+ },
+ ];
- expect( unitSelect ).toBeFalsy();
- expect( unitLabel ).toBeTruthy();
- expect( fontSize ).toBe( 16 );
+ it( 'displays a toggle group control with t-shirt sizes', () => {
+ render(
+
+ );
+ const options = screen.getAllByRole( 'radio' );
+ expect( options ).toHaveLength( 5 );
+ expect( options[ 0 ] ).toHaveTextContent( 'S' );
+ expect( options[ 0 ] ).toHaveAccessibleName( 'S' );
+ expect( options[ 1 ] ).toHaveTextContent( 'M' );
+ expect( options[ 1 ] ).toHaveAccessibleName( 'Medium' );
+ expect( options[ 2 ] ).toHaveTextContent( 'L' );
+ expect( options[ 2 ] ).toHaveAccessibleName( 'Large' );
+ expect( options[ 3 ] ).toHaveTextContent( 'XL' );
+ expect( options[ 3 ] ).toHaveAccessibleName( 'Huge' );
+ expect( options[ 4 ] ).toHaveTextContent( 'XXL' );
+ expect( options[ 4 ] ).toHaveAccessibleName( 'Gigantosaurus' );
} );
- it( 'should use units when fontSizes size values have units', () => {
- let fontSize;
- const fontSizes = [
- {
- name: 'Small',
- slug: 'small',
- size: '12px',
- },
- ];
- const setFontSize = jest.fn(
- ( nextSize ) => ( fontSize = nextSize )
- );
+ test.each( [
+ { value: undefined, expectedLabel: 'Size Default' },
+ { value: '12px', expectedLabel: 'Size small(px)' },
+ { value: '40px', expectedLabel: 'Size Gigantosaurus(px)' },
+ ] )(
+ 'displays $expectedLabel as label when value is $value',
+ ( { value, expectedLabel } ) => {
+ render(
+
+ );
+ expect(
+ screen.getByLabelText( expectedLabel )
+ ).toBeInTheDocument();
+ }
+ );
+ it( 'calls onChange when a font size is selected', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
render(
);
-
- toggleCustomInput( true );
- const unitSelect = getUnitSelect();
- const unitLabel = getUnitLabel();
- const input = screen.getByLabelText( 'Custom', {
- selector: 'input',
- } );
-
- input.focus();
- fireEvent.change( input, { target: { value: 16 } } );
-
- expect( unitSelect ).toBeTruthy();
- expect( unitLabel ).toBeFalsy();
- expect( fontSize ).toBe( '16px' );
+ await user.click( screen.getByRole( 'radio', { name: 'Medium' } ) );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( '16px' );
} );
+
+ commonTests( fontSizes );
} );
- describe( 'renders different control', () => {
- const options = [
+
+ describe( 'with ≤ 5 heterogeneous font sizes', () => {
+ const fontSizes = [
{
- name: 'Small',
slug: 'small',
- size: '0.65rem',
+ name: 'Small',
+ size: '12px',
},
{
- name: 'Medium',
slug: 'medium',
- size: '1.125rem',
+ name: 'Medium',
+ size: '1em',
},
{
- name: 'Large',
slug: 'large',
- size: '1.7rem',
+ name: 'Large',
+ size: '2rem',
+ },
+ {
+ slug: 'x-large',
+ name: 'Extra Large',
+ size: 'clamp(1.75rem, 3vw, 2.25rem)',
},
];
- it( 'should render select control when we have more than five font sizes', () => {
- const extraOptions = [
- {
- name: 'Extra Large',
- slug: 'extra-large',
- size: '1.95rem',
- },
- {
- name: 'Extra Extra Large',
- slug: 'extra-extra-large',
- size: '2.5rem',
- },
- {
- name: 'Huge',
- slug: 'huge',
- size: '2.8rem',
- },
- ];
- const fontSizes = [ ...options, ...extraOptions ];
+
+ it( 'displays a toggle group control with t-shirt sizes', () => {
render(
);
- // Trigger click to open the select menu and take into account
- // the two extra options (default, custom);
- fireEvent.click(
- screen.getByLabelText( 'Font size', { selector: 'button' } )
- );
- const element = screen.getAllByRole( 'option' );
- expect( element ).toHaveLength( fontSizes.length + 2 );
+ const options = screen.getAllByRole( 'radio' );
+ expect( options ).toHaveLength( 4 );
+ expect( options[ 0 ] ).toHaveTextContent( 'S' );
+ expect( options[ 0 ] ).toHaveAccessibleName( 'Small' );
+ expect( options[ 1 ] ).toHaveTextContent( 'M' );
+ expect( options[ 1 ] ).toHaveAccessibleName( 'Medium' );
+ expect( options[ 2 ] ).toHaveTextContent( 'L' );
+ expect( options[ 2 ] ).toHaveAccessibleName( 'Large' );
+ expect( options[ 3 ] ).toHaveTextContent( 'XL' );
+ expect( options[ 3 ] ).toHaveAccessibleName( 'Extra Large' );
} );
- describe( 'segmented control', () => {
- it( 'should use t-shirt labels for simple css values', () => {
- const fontSizes = [ ...options ];
+
+ test.each( [
+ { value: undefined, expectedLabel: 'Size Default' },
+ { value: '12px', expectedLabel: 'Size Small' },
+ { value: '1em', expectedLabel: 'Size Medium' },
+ { value: '2rem', expectedLabel: 'Size Large' },
+ {
+ value: 'clamp(1.75rem, 3vw, 2.25rem)',
+ expectedLabel: 'Size Extra Large',
+ },
+ ] )(
+ 'displays $expectedLabel as label when value is $value',
+ ( { value, expectedLabel } ) => {
render(
- );
- const element = screen.getByLabelText( 'Large' );
- expect( element ).toBeInTheDocument();
- expect( element.children[ 0 ] ).toHaveTextContent( 'L' );
- } );
- it( 'should use incremental sequence of t-shirt sizes as labels if we have complex css', () => {
- const fontSizes = [
- ...options,
- {
- name: 'Extra Large',
- slug: 'extra-large',
- size: 'clamp(1.75rem, 3vw, 2.25rem)',
- },
- ];
- render(
-
);
- const largeElement = screen.getByLabelText( 'Large' );
- expect( largeElement ).toBeInTheDocument();
- expect( largeElement ).toHaveTextContent( 'L' );
+ expect(
+ screen.getByLabelText( expectedLabel )
+ ).toBeInTheDocument();
+ }
+ );
- const extraLargeElement =
- screen.getByLabelText( 'Extra Large' );
- expect( extraLargeElement ).toBeInTheDocument();
- expect( extraLargeElement.children[ 0 ] ).toHaveTextContent(
- 'XL'
- );
- } );
- it( 'should use font size `slug` for for header hint label by default', () => {
- const fontSizes = [
- {
- name: 'Allosaurus Large',
- slug: 'allosaurus-l',
- size: '20rem',
- },
- ];
+ test.each( [
+ { radio: 'Small', expectedValue: '12px' },
+ { radio: 'Medium', expectedValue: '1em' },
+ { radio: 'Large', expectedValue: '2rem' },
+ {
+ radio: 'Extra Large',
+ expectedValue: 'clamp(1.75rem, 3vw, 2.25rem)',
+ },
+ ] )(
+ 'calls onChange( $expectedValue ) when $radio is selected',
+ async ( { radio, expectedValue } ) => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
render(
);
-
- const largeFontSizeElement = screen.getByLabelText(
- 'Size Allosaurus Large(rem)'
+ await user.click(
+ screen.getByRole( 'radio', { name: radio } )
);
- expect( largeFontSizeElement ).toBeInTheDocument();
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( expectedValue );
+ }
+ );
+
+ commonTests( fontSizes );
+ } );
+
+ function commonTests( fontSizes: FontSize[] ) {
+ it( 'allows custom values by default', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
} );
- it( 'should fallback to font size `slug` for header hint label if `name` is undefined', () => {
- const fontSizes = [
- {
- slug: 'gigantosaurus',
- size: '1000px',
- },
- ];
- render(
-
- );
+ const onChange = jest.fn();
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ await user.type( screen.getByLabelText( 'Custom' ), '80' );
+ expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke.
+ expect( onChange ).toHaveBeenCalledWith( '80px' );
+ } );
- const giganticFontSizeElement = screen.getByLabelText(
- 'Size gigantosaurus(px)'
- );
- expect( giganticFontSizeElement ).toBeInTheDocument();
+ it( 'does not allow custom values when disableCustomFontSizes is set', () => {
+ render(
+
+ );
+ expect(
+ screen.queryByRole( 'button', { name: 'Set custom size' } )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'does not display a slider by default', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
} );
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ expect(
+ screen.queryByLabelText( 'Custom Size' )
+ ).not.toBeInTheDocument();
} );
- } );
+
+ it( 'allows a slider to be used when withSlider is set', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ const sliderInput = screen.getByLabelText( 'Custom Size' );
+ fireEvent.change( sliderInput, {
+ target: { value: 80 },
+ } );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( '80px' );
+ } );
+
+ it( 'allows reset by default', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ const onChange = jest.fn();
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ await user.click( screen.getByRole( 'button', { name: 'Reset' } ) );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( undefined );
+ } );
+
+ it( 'does not allow reset when withReset is false', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+ render(
+
+ );
+ await user.click(
+ screen.getByRole( 'button', { name: 'Set custom size' } )
+ );
+ expect(
+ screen.queryByRole( 'button', { name: 'Reset' } )
+ ).not.toBeInTheDocument();
+ } );
+ }
} );