+
+
- document.body.querySelector( '.components-unit-control' );
-const getInput = () =>
- document.body.querySelector( '.components-unit-control input' );
-const getSelect = () =>
- document.body.querySelector( '.components-unit-control select' );
-const getUnitLabel = () =>
- document.body.querySelector( '.components-unit-control__unit-label' );
-
-const fireKeyDown = ( data ) =>
- fireEvent.keyDown( document.activeElement || document.body, data );
+// TODO: What determines spinbutton or textbox ?
+const getInput = ( isNumeric: boolean = false ) =>
+ screen.getByRole(
+ isNumeric ? 'spinbutton' : 'textbox'
+ ) as HTMLInputElement;
+const getSelect = () => screen.getByRole( 'combobox' ) as HTMLSelectElement;
+const getSelectOptions = () =>
+ screen.getAllByRole( 'option' ) as HTMLOptionElement[];
const ControlledSyncUnits = () => {
- const [ state, setState ] = useState( { valueA: '', valueB: '' } );
+ const [ state, setState ] = useState( {
+ valueA: '',
+ valueB: '',
+ } );
// Keep the unit sync'd between the two `UnitControl` instances.
- const onUnitControlChange = ( fieldName, newValue ) => {
- // eslint-disable-next-line @wordpress/no-unused-vars-before-return
- const [ quantity, newUnit ] = parseQuantityAndUnitFromRawValue(
+ const onUnitControlChange = (
+ fieldName: 'valueA' | 'valueB',
+ newValue?: string | number
+ ) => {
+ const parsedQuantityAndUnit = parseQuantityAndUnitFromRawValue(
newValue
);
+ const quantity = parsedQuantityAndUnit[ 0 ];
if ( ! Number.isFinite( quantity ) ) {
return;
}
- const nextState = { ...state, [ fieldName ]: newValue };
+ const newUnit = parsedQuantityAndUnit[ 1 ];
+
+ const nextState = {
+ ...state,
+ [ fieldName ]: newValue,
+ };
Object.entries( state ).forEach( ( [ stateProp, stateValue ] ) => {
const [
@@ -50,7 +60,9 @@ const ControlledSyncUnits = () => {
] = parseQuantityAndUnitFromRawValue( stateValue );
if ( stateProp !== fieldName && stateUnit !== newUnit ) {
- nextState[ stateProp ] = `${ stateQuantity }${ newUnit }`;
+ nextState[
+ stateProp as 'valueA' | 'valueB'
+ ] = `${ stateQuantity }${ newUnit }`;
}
} );
@@ -77,38 +89,46 @@ describe( 'UnitControl', () => {
describe( 'Basic rendering', () => {
it( 'should render', () => {
render( );
- const input = getInput();
+ const input = getInput( true );
const select = getSelect();
- expect( input ).toBeTruthy();
- expect( select ).toBeTruthy();
+ expect( input ).toBeInTheDocument();
+ expect( select ).toBeInTheDocument();
} );
+ // TODO: update TS matchers
it( 'should render custom className', () => {
- render( );
+ const { container: noClassName } = render( );
- const el = getComponent();
+ const { container: withClassName } = render(
+
+ );
- expect( el.classList.contains( 'hello' ) ).toBe( true );
+ expect( noClassName.firstChild ).toMatchDiffSnapshot(
+ withClassName.firstChild
+ );
} );
+ // TODO: update TS matchers
it( 'should not render select, if units are disabled', () => {
render( );
- const input = getInput();
- const select = getSelect();
+ const input = getInput( true );
+ const select = screen.queryByRole( 'combobox' );
- expect( input ).toBeTruthy();
- expect( select ).toBeFalsy();
+ expect( input ).toBeInTheDocument();
+ expect( select ).not.toBeInTheDocument();
} );
- it( 'should render label if single units', () => {
+ // TODO: update TS matchers
+ // Check why it errors
+ it.skip( 'should render label if single units', () => {
render( );
- const select = getSelect();
- const label = getUnitLabel();
+ const select = screen.queryByRole( 'combobox' );
+ const label = screen.getByText( '%' );
- expect( select ).toBeFalsy();
- expect( label ).toBeTruthy();
+ expect( select ).not.toBeInTheDocument();
+ expect( label ).toBeInTheDocument();
} );
} );
@@ -119,7 +139,7 @@ describe( 'UnitControl', () => {
render( );
- const input = getInput();
+ const input = getInput( true );
input.focus();
fireEvent.change( input, { target: { value: 62 } } );
@@ -128,56 +148,65 @@ describe( 'UnitControl', () => {
} );
it( 'should increment value on UP press', () => {
- let state = '50px';
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | undefined = '50px';
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render( );
- getInput().focus();
- fireKeyDown( { keyCode: UP } );
+ const input = getInput( true );
+ input.focus();
+ fireEvent.keyDown( input, { keyCode: UP } );
expect( state ).toBe( '51px' );
} );
it( 'should increment value on UP + SHIFT press, with step', () => {
- let state = '50px';
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | undefined = '50px';
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render( );
- getInput().focus();
- fireKeyDown( { keyCode: UP, shiftKey: true } );
+ const input = getInput( true );
+ input.focus();
+ fireEvent.keyDown( input, { keyCode: UP, shiftKey: true } );
expect( state ).toBe( '60px' );
} );
it( 'should decrement value on DOWN press', () => {
- let state = 50;
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | number | undefined = 50;
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render( );
- getInput().focus();
- fireKeyDown( { keyCode: DOWN } );
+ const input = getInput( true );
+ input.focus();
+ fireEvent.keyDown( input, { keyCode: DOWN } );
expect( state ).toBe( '49px' );
} );
it( 'should decrement value on DOWN + SHIFT press, with step', () => {
- let state = 50;
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | number | undefined = 50;
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render( );
- getInput().focus();
- fireKeyDown( { keyCode: DOWN, shiftKey: true } );
+ const input = getInput( true );
+ input.focus();
+ fireEvent.keyDown( input, { keyCode: DOWN, shiftKey: true } );
expect( state ).toBe( '40px' );
} );
it( 'should cancel change when ESCAPE key is pressed', () => {
- let state = 50;
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | number | undefined = 50;
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render(
{
/>
);
- const input = getInput();
+ const input = getInput() as HTMLInputElement;
input.focus();
fireEvent.change( input, { target: { value: '300px' } } );
@@ -195,7 +224,7 @@ describe( 'UnitControl', () => {
expect( input.value ).toBe( '300px' );
expect( state ).toBe( 50 );
- fireKeyDown( { keyCode: ESCAPE } );
+ fireEvent.keyDown( input, { keyCode: ESCAPE } );
expect( input.value ).toBe( '50' );
expect( state ).toBe( 50 );
@@ -204,8 +233,9 @@ describe( 'UnitControl', () => {
describe( 'Unit', () => {
it( 'should update unit value on change', () => {
- let state = 'px';
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | undefined = 'px';
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
render( );
@@ -224,8 +254,7 @@ describe( 'UnitControl', () => {
render( );
- const select = getSelect();
- const options = select.querySelectorAll( 'option' );
+ const options = getSelectOptions();
expect( options.length ).toBe( 2 );
@@ -236,8 +265,9 @@ describe( 'UnitControl', () => {
} );
it( 'should reset value on unit change, if unit has default value', () => {
- let state = 50;
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | number | undefined = 50;
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
const units = [
{ value: 'pt', label: 'pt', default: 25 },
@@ -266,8 +296,9 @@ describe( 'UnitControl', () => {
} );
it( 'should not reset value on unit change, if disabled', () => {
- let state = 50;
- const setState = ( nextState ) => ( state = nextState );
+ let state: string | number | undefined = 50;
+ const setState: UnitControlOnChangeCallback = ( nextState ) =>
+ ( state = nextState );
const units = [
{ value: 'pt', label: 'pt', default: 25 },
@@ -296,8 +327,9 @@ describe( 'UnitControl', () => {
} );
it( 'should set correct unit if single units', () => {
- let state = '50%';
- const setState = ( value ) => ( state = value );
+ let state: string | undefined = '50%';
+ const setState: UnitControlOnChangeCallback = ( value ) =>
+ ( state = value );
render(
{
/>
);
- const input = getInput();
+ const input = getInput( true );
input.focus();
fireEvent.change( input, { target: { value: 62 } } );
expect( state ).toBe( '62%' );
} );
+ // TODO: update TS matchers
it( 'should update unit value when a new raw value is passed', () => {
render( );
@@ -357,7 +390,7 @@ describe( 'UnitControl', () => {
const input = getInput();
input.focus();
fireEvent.change( input, { target: { value: '55 em' } } );
- fireKeyDown( { keyCode: ENTER } );
+ fireEvent.keyDown( input, { keyCode: ENTER } );
expect( state ).toBe( '55em' );
} );
@@ -374,7 +407,7 @@ describe( 'UnitControl', () => {
const input = getInput();
input.focus();
fireEvent.change( input, { target: { value: '61 PX' } } );
- fireKeyDown( { keyCode: ENTER } );
+ fireEvent.keyDown( input, { keyCode: ENTER } );
expect( state ).toBe( '61px' );
} );
@@ -391,7 +424,7 @@ describe( 'UnitControl', () => {
const input = getInput();
input.focus();
fireEvent.change( input, { target: { value: '55 em' } } );
- fireKeyDown( { keyCode: ENTER } );
+ fireEvent.keyDown( input, { keyCode: ENTER } );
expect( state ).toBe( '55em' );
} );
@@ -408,7 +441,7 @@ describe( 'UnitControl', () => {
const input = getInput();
input.focus();
fireEvent.change( input, { target: { value: '-10 %' } } );
- fireKeyDown( { keyCode: ENTER } );
+ fireEvent.keyDown( input, { keyCode: ENTER } );
expect( state ).toBe( '-10%' );
} );
@@ -427,7 +460,7 @@ describe( 'UnitControl', () => {
fireEvent.change( input, {
target: { value: '123 rEm ' },
} );
- fireKeyDown( { keyCode: ENTER } );
+ fireEvent.keyDown( input, { keyCode: ENTER } );
expect( state ).toBe( '123rem' );
} );
@@ -435,7 +468,7 @@ describe( 'UnitControl', () => {
it( 'should update unit after initial render and with new unit prop', () => {
const { rerender } = render( );
- const select = getSelect();
+ const select = getSelect() as HTMLSelectElement;
expect( select.value ).toBe( '%' );
@@ -447,7 +480,8 @@ describe( 'UnitControl', () => {
it( 'should fallback to default unit if parsed unit is invalid', () => {
render( );
- expect( getSelect().value ).toBe( 'px' );
+ const select = getSelect() as HTMLSelectElement;
+ expect( select.value ).toBe( 'px' );
} );
it( 'should display valid CSS unit when not explicitly included in units list', () => {
@@ -462,7 +496,7 @@ describe( 'UnitControl', () => {
);
const select = getSelect();
- const options = select.querySelectorAll( 'option' );
+ const options = getSelectOptions();
expect( select.value ).toBe( '%' );
expect( options.length ).toBe( 3 );
diff --git a/packages/components/src/unit-control/test/utils.js b/packages/components/src/unit-control/test/utils.ts
similarity index 76%
rename from packages/components/src/unit-control/test/utils.js
rename to packages/components/src/unit-control/test/utils.ts
index b5fe385716ded..7a6645d1c0256 100644
--- a/packages/components/src/unit-control/test/utils.js
+++ b/packages/components/src/unit-control/test/utils.ts
@@ -7,21 +7,28 @@ import {
getValidParsedQuantityAndUnit,
getUnitsWithCurrentUnit,
} from '../utils';
+import type { WPUnitControlUnit } from '../types';
describe( 'UnitControl utils', () => {
describe( 'useCustomUnits', () => {
it( 'should return filtered css units', () => {
- const cssUnits = [ { value: 'px' }, { value: '%' } ];
+ const cssUnits = [
+ { value: 'px', label: 'pixel' },
+ { value: '%', label: 'percent' },
+ ];
const units = useCustomUnits( {
availableUnits: [ 'em', 'px' ],
units: cssUnits,
} );
- expect( units ).toEqual( [ { value: 'px' } ] );
+ expect( units ).toEqual( [ { value: 'px', label: 'pixel' } ] );
} );
it( 'should add default values to available units', () => {
- const cssUnits = [ { value: 'px' }, { value: '%' } ];
+ const cssUnits = [
+ { value: 'px', label: 'pixel' },
+ { value: '%', label: 'percent' },
+ ];
const units = useCustomUnits( {
availableUnits: [ '%', 'px' ],
defaultValues: { '%': 10, px: 10 },
@@ -29,8 +36,8 @@ describe( 'UnitControl utils', () => {
} );
expect( units ).toEqual( [
- { value: 'px', default: 10 },
- { value: '%', default: 10 },
+ { value: 'px', label: 'pixel', default: 10 },
+ { value: '%', label: 'percent', default: 10 },
] );
} );
@@ -38,24 +45,32 @@ describe( 'UnitControl utils', () => {
// Although the public APIs of the component expect a `number` as the type of the
// default values, it's still good to test for strings (as it can happen in un-typed
// environments)
- const cssUnits = [ { value: 'px' }, { value: '%' } ];
+ const cssUnits = [
+ { value: 'px', label: 'pixel' },
+ { value: '%', label: 'percent' },
+ ];
const units = useCustomUnits( {
availableUnits: [ '%', 'px' ],
defaultValues: {
+ // @ts-ignore (passing a string instead of a number is the point of the test)
'%': '14',
+ // @ts-ignore (passing a string instead of a number is the point of the test)
px: 'not a valid numeric quantity',
},
units: cssUnits,
} );
expect( units ).toEqual( [
- { value: 'px', default: undefined },
- { value: '%', default: 14 },
+ { value: 'px', label: 'pixel', default: undefined },
+ { value: '%', label: 'percent', default: 14 },
] );
} );
it( 'should return an empty array where availableUnits match no preferred css units', () => {
- const cssUnits = [ { value: 'em' }, { value: 'vh' } ];
+ const cssUnits = [
+ { value: 'em', label: 'em' },
+ { value: 'vh', label: 'vh' },
+ ];
const units = useCustomUnits( {
availableUnits: [ '%', 'px' ],
defaultValues: { '%': 10, px: 10 },
@@ -69,16 +84,19 @@ describe( 'UnitControl utils', () => {
describe( 'filterUnitsWithSettings', () => {
it( 'should return filtered units array', () => {
const preferredUnits = [ '%', 'px' ];
- const availableUnits = [ { value: 'px' }, { value: 'em' } ];
+ const availableUnits = [
+ { value: 'px', label: 'pixel' },
+ { value: 'em', label: 'em' },
+ ];
expect(
filterUnitsWithSettings( preferredUnits, availableUnits )
- ).toEqual( [ { value: 'px' } ] );
+ ).toEqual( [ { value: 'px', label: 'pixel' } ] );
} );
it( 'should return empty array where preferred units match no available css unit', () => {
const preferredUnits = [ '%', 'px' ];
- const availableUnits = [ { value: 'em' } ];
+ const availableUnits = [ { value: 'em', label: 'em' } ];
expect(
filterUnitsWithSettings( preferredUnits, availableUnits )
@@ -92,13 +110,14 @@ describe( 'UnitControl utils', () => {
const availableUnits = false;
expect(
+ // @ts-ignore (passing `false` instead of a valid array of units is the point of the test)
filterUnitsWithSettings( preferredUnits, availableUnits )
).toEqual( [] );
} );
it( 'should return empty array where available units is set to an empty array', () => {
const preferredUnits = [ '%', 'px' ];
- const availableUnits = [];
+ const availableUnits: WPUnitControlUnit[] = [];
expect(
filterUnitsWithSettings( preferredUnits, availableUnits )
@@ -127,7 +146,7 @@ describe( 'UnitControl utils', () => {
it( 'should return fallback value', () => {
const nextValue = 'thirteen';
- const preferredUnits = [ { value: 'em' } ];
+ const preferredUnits = [ { value: 'em', label: 'em' } ];
const fallbackValue = 13;
expect(
@@ -155,7 +174,7 @@ describe( 'UnitControl utils', () => {
it( 'should return first unit in preferred units collection as second fallback unit', () => {
const nextValue = 101;
- const preferredUnits = [ { value: 'px' } ];
+ const preferredUnits = [ { value: 'px', label: 'pixel' } ];
expect(
getValidParsedQuantityAndUnit( nextValue, preferredUnits )
@@ -185,8 +204,8 @@ describe( 'UnitControl utils', () => {
expect( result ).toHaveLength( 3 );
const currentUnit = result.shift();
- expect( currentUnit.value ).toBe( '%' );
- expect( currentUnit.label ).toBe( '%' );
+ expect( currentUnit?.value ).toBe( '%' );
+ expect( currentUnit?.label ).toBe( '%' );
expect( result ).toEqual( limitedUnits );
} );
@@ -196,8 +215,8 @@ describe( 'UnitControl utils', () => {
expect( result ).toHaveLength( 3 );
const currentUnit = result.shift();
- expect( currentUnit.value ).toBe( '%' );
- expect( currentUnit.label ).toBe( '%' );
+ expect( currentUnit?.value ).toBe( '%' );
+ expect( currentUnit?.label ).toBe( '%' );
expect( result ).toEqual( limitedUnits );
} );
diff --git a/packages/components/src/unit-control/utils.ts b/packages/components/src/unit-control/utils.ts
index 87c0142ec9774..d992db157f149 100644
--- a/packages/components/src/unit-control/utils.ts
+++ b/packages/components/src/unit-control/utils.ts
@@ -155,7 +155,7 @@ export function getParsedQuantityAndUnit(
* @param units List of units.
* @return Whether the list actually contains any units.
*/
-export function hasUnits( units?: WPUnitControlUnit[] ): boolean {
+export function hasUnits( units?: WPUnitControlUnit[] ) {
// Although the `isArray` check shouldn't be necessary (given the signature of
// this typed function), it's better to stay on the side of caution, since
// this function may be called from un-typed environments.
@@ -218,7 +218,7 @@ export function parseQuantityAndUnitFromRawValue(
*/
export function getValidParsedQuantityAndUnit(
rawValue: string | number,
- allowedUnits: WPUnitControlUnit[],
+ allowedUnits?: WPUnitControlUnit[],
fallbackQuantity?: number,
fallbackUnit?: string
): [ number | undefined, string | undefined ] {
@@ -236,7 +236,9 @@ export function getValidParsedQuantityAndUnit(
let unitToReturn = parsedUnit || fallbackUnit;
if ( ! unitToReturn && hasUnits( allowedUnits ) ) {
- unitToReturn = allowedUnits[ 0 ].value;
+ // The `hasUnit` check guarantees that `allowedUnits`, for the next line,
+ // is an array with at least one item.
+ unitToReturn = ( allowedUnits as WPUnitControlUnit[] )[ 0 ].value;
}
return [ quantityToReturn, unitToReturn ];
@@ -295,7 +297,7 @@ export const useCustomUnits = ( {
}: {
units?: WPUnitControlUnit[];
availableUnits?: string[];
- defaultValues: Record< string, number >;
+ defaultValues?: Record< string, number >;
} ): WPUnitControlUnit[] => {
const customUnitsToReturn = filterUnitsWithSettings(
availableUnits,
diff --git a/test/unit/config/testing-library.js b/test/unit/config/testing-library.js
index dd8c725c6f4ae..5b29b671c5ebe 100644
--- a/test/unit/config/testing-library.js
+++ b/test/unit/config/testing-library.js
@@ -1,3 +1,4 @@
require( '@testing-library/jest-dom' );
+require( 'snapshot-diff' );
require( 'snapshot-diff/extend-expect' );
require( './matchers/to-match-style-diff-snapshot' );
diff --git a/tsconfig.json b/tsconfig.json
index 53ab6c77aa41b..2df18b596bb4c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -32,7 +32,8 @@
{ "path": "packages/token-list" },
{ "path": "packages/url" },
{ "path": "packages/warning" },
- { "path": "packages/wordcount" }
+ { "path": "packages/wordcount" },
+ { "path": "./tsconfig.test.json" }
],
"files": []
}
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 0000000000000..56ab3c3f03681
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,50 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "emitDeclarationOnly": false,
+ "types": [
+ "gutenberg-env",
+ "jest",
+ "@testing-library/jest-dom",
+ "snapshot-diff",
+ "@emotion/core"
+ ],
+ },
+ "references": [
+ { "path": "packages/a11y" },
+ { "path": "packages/api-fetch" },
+ { "path": "packages/autop" },
+ { "path": "packages/blob" },
+ { "path": "packages/block-editor" },
+ { "path": "packages/components" },
+ { "path": "packages/compose" },
+ { "path": "packages/data" },
+ { "path": "packages/date" },
+ { "path": "packages/deprecated" },
+ { "path": "packages/docgen" },
+ { "path": "packages/dom" },
+ { "path": "packages/element" },
+ { "path": "packages/dom-ready" },
+ { "path": "packages/escape-html" },
+ { "path": "packages/eslint-plugin" },
+ { "path": "packages/html-entities" },
+ { "path": "packages/hooks" },
+ { "path": "packages/i18n" },
+ { "path": "packages/icons" },
+ { "path": "packages/is-shallow-equal" },
+ { "path": "packages/keycodes" },
+ { "path": "packages/lazy-import" },
+ { "path": "packages/prettier-config" },
+ { "path": "packages/primitives" },
+ { "path": "packages/priority-queue" },
+ { "path": "packages/project-management-automation" },
+ { "path": "packages/react-i18n" },
+ { "path": "packages/token-list" },
+ { "path": "packages/url" },
+ { "path": "packages/warning" },
+ { "path": "packages/wordcount" }
+ ],
+ "include": [ "packages/**/test/*.ts", "packages/**/test/*.tsx" ],
+ "exclude": []
+}