diff --git a/packages/components/src/unit-control/test/__snapshots__/index.tsx.snap b/packages/components/src/unit-control/test/__snapshots__/index.tsx.snap new file mode 100644 index 0000000000000..8caf79556bd9b --- /dev/null +++ b/packages/components/src/unit-control/test/__snapshots__/index.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnitControl Basic rendering should render custom className 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -1,10 +1,10 @@ +
+
+
+ - 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": [] +}