From 9091ba6da68bfc5d0ed5be2902bd5f1cc636d5c4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 27 Mar 2023 10:32:35 +0100 Subject: [PATCH 1/2] Use immutableSet utility to set style properties --- .../components/global-styles/color-panel.js | 226 +++++++----------- .../global-styles/dimensions-panel.js | 55 +---- .../global-styles/typography-panel.js | 77 ++---- packages/block-editor/src/hooks/test/utils.js | 104 -------- packages/block-editor/src/hooks/utils.js | 71 +----- packages/block-editor/src/utils/object.js | 69 ++++++ .../block-editor/src/utils/test/object.js | 107 +++++++++ 7 files changed, 304 insertions(+), 405 deletions(-) create mode 100644 packages/block-editor/src/utils/object.js create mode 100644 packages/block-editor/src/utils/test/object.js diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 856573d11bd5d5..ba8a97b1708584 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -28,6 +28,7 @@ import { __ } from '@wordpress/i18n'; import ColorGradientControl from '../colors-gradients/control'; import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; import { getValueFromVariable } from './utils'; +import { immutableSet } from '../../utils/object'; export function useHasColorPanel( settings ) { const hasTextPanel = useHasTextPanel( settings ); @@ -328,10 +329,13 @@ export default function ColorPanel( { const userTextColor = decodeValue( value?.color?.text ); const hasTextColor = () => !! userTextColor; const setTextColor = ( newColor ) => { - onChange( { - ...value, - color: { ...value?.color, text: encodeColorValue( newColor ) }, - } ); + onChange( + immutableSet( + value, + [ 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); }; const resetTextColor = () => setTextColor( undefined ); @@ -343,34 +347,31 @@ export default function ColorPanel( { const userGradient = decodeValue( value?.color?.gradient ); const hasBackground = () => !! userBackgroundColor || !! userGradient; const setBackgroundColor = ( newColor ) => { - onChange( { - ...value, - color: { - ...value?.color, - background: encodeColorValue( newColor ), - gradient: undefined, - }, - } ); + const newValue = immutableSet( + value, + [ 'color', 'background' ], + encodeColorValue( newColor ) + ); + newValue.color.gradient = undefined; + onChange( newValue ); }; const setGradient = ( newGradient ) => { - onChange( { - ...value, - color: { - ...value?.color, - background: undefined, - gradient: encodeGradientValue( newGradient ), - }, - } ); + const newValue = immutableSet( + value, + [ 'color', 'gradient' ], + encodeGradientValue( newGradient ) + ); + newValue.color.background = undefined; + onChange( newValue ); }; const resetBackground = () => { - onChange( { - ...value, - color: { - ...value?.color, - background: undefined, - gradient: undefined, - }, - } ); + const newValue = immutableSet( + value, + [ 'color', 'background' ], + undefined + ); + newValue.color.gradient = undefined; + onChange( newValue ); }; // Links @@ -380,19 +381,13 @@ export default function ColorPanel( { ); const userLinkColor = decodeValue( value?.elements?.link?.color?.text ); const setLinkColor = ( newColor ) => { - onChange( { - ...value, - elements: { - ...value?.elements, - link: { - ...value?.elements?.link, - color: { - ...value?.elements?.link?.color, - text: encodeColorValue( newColor ), - }, - }, - }, - } ); + onChange( + immutableSet( + value, + [ 'elements', 'link', 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); }; const hoverLinkColor = decodeValue( inheritedValue?.elements?.link?.[ ':hover' ]?.color?.text @@ -401,46 +396,28 @@ export default function ColorPanel( { value?.elements?.link?.[ ':hover' ]?.color?.text ); const setHoverLinkColor = ( newColor ) => { - onChange( { - ...value, - elements: { - ...value?.color?.elements, - link: { - ...value?.elements?.link, - ':hover': { - ...value?.elements?.link?.[ ':hover' ], - color: { - ...value?.elements?.link?.[ ':hover' ]?.color, - text: encodeColorValue( newColor ), - }, - }, - }, - }, - } ); + onChange( + immutableSet( + value, + [ 'elements', 'link', ':hover', 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); }; const hasLink = () => !! userLinkColor || !! userHoverLinkColor; - const resetLink = () => - onChange( { - ...value, - elements: { - ...value?.color?.elements, - link: { - ...value?.color?.elements?.link, - color: { - ...value?.color?.elements?.link?.color, - text: undefined, - }, - ':hover': { - ...value?.color?.elements?.link?.[ ':hover' ], - color: { - ...value?.color?.elements?.link?.[ ':hover' ] - ?.color, - text: undefined, - }, - }, - }, - }, - } ); + const resetLink = () => { + let newValue = immutableSet( + value, + [ 'elements', 'link', ':hover', 'color', 'text' ], + undefined + ); + newValue = immutableSet( + newValue, + [ 'elements', 'link', 'color', 'text' ], + undefined + ); + onChange( newValue ); + }; // Elements const elements = [ @@ -614,69 +591,42 @@ export default function ColorPanel( { elementGradientUserColor ); const resetElement = () => { - onChange( { - ...value, - elements: { - ...value?.elements, - [ name ]: { - ...value?.elements?.[ name ], - color: { - ...value?.elements?.[ name ]?.color, - background: undefined, - gradient: undefined, - text: undefined, - }, - }, - }, - } ); + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'background' ], + undefined + ); + newValue.elements[ name ].color.gradient = undefined; + newValue.elements[ name ].color.text = undefined; + onChange( newValue ); }; - const setElementTextColor = ( newValue ) => { - onChange( { - ...value, - elements: { - ...value?.elements, - [ name ]: { - ...value?.elements?.[ name ], - color: { - ...value?.elements?.[ name ]?.color, - text: encodeColorValue( newValue ), - }, - }, - }, - } ); + const setElementTextColor = ( newTextColor ) => { + onChange( + immutableSet( + value, + [ 'elements', name, 'color', 'text' ], + encodeColorValue( newTextColor ) + ) + ); }; - const setElementBackgroundColor = ( newValue ) => { - onChange( { - ...value, - elements: { - ...value?.elements, - [ name ]: { - ...value?.elements?.[ name ], - color: { - ...value?.elements?.[ name ]?.color, - background: encodeColorValue( newValue ), - gradient: undefined, - }, - }, - }, - } ); + const setElementBackgroundColor = ( newBackgroundColor ) => { + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'background' ], + encodeColorValue( newBackgroundColor ) + ); + newValue.elements[ name ].color.gradient = undefined; + onChange( newValue ); }; - const setElementGradient = ( newValue ) => { - onChange( { - ...value, - elements: { - ...value?.elements, - [ name ]: { - ...value?.elements?.[ name ], - color: { - ...value?.elements?.[ name ]?.color, - gradient: encodeGradientValue( newValue ), - background: undefined, - }, - }, - }, - } ); + const setElementGradient = ( newGradient ) => { + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'gradient' ], + encodeGradientValue( newGradient ) + ); + newValue.elements[ name ].color.background = undefined; + onChange( newValue ); }; const supportsTextColor = true; // Background color is not supported for `caption` diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index b63d9c5d063739..c876ca2d1c1b66 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -28,6 +28,7 @@ import SpacingSizesControl from '../spacing-sizes-control'; import HeightControl from '../height-control'; import ChildLayoutControl from '../child-layout-control'; import { cleanEmptyObject } from '../../hooks/utils'; +import { immutableSet } from '../../utils/object'; const AXIAL_SIDES = [ 'horizontal', 'vertical' ]; @@ -223,13 +224,9 @@ export default function DimensionsPanel( { useHasContentSize( settings ) && includeLayoutControls; const contentSizeValue = decodeValue( inheritedValue?.layout?.contentSize ); const setContentSizeValue = ( newValue ) => { - onChange( { - ...value, - layout: { - ...value?.layout, - contentSize: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'layout', 'contentSize' ], newValue ) + ); }; const hasUserSetContentSizeValue = () => !! value?.layout?.contentSize; const resetContentSizeValue = () => setContentSizeValue( undefined ); @@ -239,13 +236,7 @@ export default function DimensionsPanel( { useHasWideSize( settings ) && includeLayoutControls; const wideSizeValue = decodeValue( inheritedValue?.layout?.wideSize ); const setWideSizeValue = ( newValue ) => { - onChange( { - ...value, - layout: { - ...value?.layout, - wideSize: newValue, - }, - } ); + onChange( immutableSet( value, [ 'layout', 'wideSize' ], newValue ) ); }; const hasUserSetWideSizeValue = () => !! value?.layout?.wideSize; const resetWideSizeValue = () => setWideSizeValue( undefined ); @@ -262,13 +253,7 @@ export default function DimensionsPanel( { paddingSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setPaddingValues = ( newPaddingValues ) => { const padding = filterValuesBySides( newPaddingValues, paddingSides ); - onChange( { - ...value, - spacing: { - ...value?.spacing, - padding, - }, - } ); + onChange( immutableSet( value, [ 'spacing', 'padding' ], padding ) ); }; const hasPaddingValue = () => !! value?.spacing?.padding && @@ -288,13 +273,7 @@ export default function DimensionsPanel( { marginSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setMarginValues = ( newMarginValues ) => { const margin = filterValuesBySides( newMarginValues, marginSides ); - onChange( { - ...value, - spacing: { - ...value?.spacing, - margin, - }, - } ); + onChange( immutableSet( value, [ 'spacing', 'margin' ], margin ) ); }; const hasMarginValue = () => !! value?.spacing?.margin && @@ -312,13 +291,9 @@ export default function DimensionsPanel( { const isAxialGap = gapSides && gapSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setGapValue = ( newGapValue ) => { - onChange( { - ...value, - spacing: { - ...value?.spacing, - blockGap: newGapValue, - }, - } ); + onChange( + immutableSet( value, [ 'spacing', 'blockGap' ], newGapValue ) + ); }; const setGapValues = ( nextBoxGapValue ) => { if ( ! nextBoxGapValue ) { @@ -341,13 +316,9 @@ export default function DimensionsPanel( { const showMinHeightControl = useHasMinHeight( settings ); const minHeightValue = decodeValue( inheritedValue?.dimensions?.minHeight ); const setMinHeightValue = ( newValue ) => { - onChange( { - ...value, - dimensions: { - ...value?.dimensions, - minHeight: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'dimensions', 'minHeight' ], newValue ) + ); }; const resetMinHeightValue = () => { setMinHeightValue( undefined ); diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 1c3c253c7b69db..cdaa6826ddd431 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -20,6 +20,7 @@ import LetterSpacingControl from '../letter-spacing-control'; import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; import { getValueFromVariable } from './utils'; +import { immutableSet } from '../../utils/object'; const MIN_TEXT_COLUMNS = 1; const MAX_TEXT_COLUMNS = 6; @@ -161,15 +162,13 @@ export default function TypographyPanel( { const slug = fontFamilies?.find( ( { fontFamily: f } ) => f === newValue )?.slug; - onChange( { - ...value, - typography: { - ...value?.typography, - fontFamily: slug - ? `var:preset|font-family|${ slug }` - : newValue, - }, - } ); + onChange( + immutableSet( + value, + [ 'typography', 'fontFamily' ], + slug ? `var:preset|font-family|${ slug }` : newValue + ) + ); }; const hasFontFamily = () => !! value?.typography?.fontFamily; const resetFontFamily = () => setFontFamily( undefined ); @@ -188,13 +187,9 @@ export default function TypographyPanel( { ? `var:preset|font-size|${ metadata?.slug }` : newValue; - onChange( { - ...value, - typography: { - ...value?.typography, - fontSize: actualValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'fontSize' ], actualValue ) + ); }; const hasFontSize = () => !! value?.typography?.fontSize; const resetFontSize = () => setFontSize( undefined ); @@ -229,13 +224,9 @@ export default function TypographyPanel( { const hasLineHeightEnabled = useHasLineHeightControl( settings ); const lineHeight = decodeValue( inheritedValue?.typography?.lineHeight ); const setLineHeight = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - lineHeight: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'lineHeight' ], newValue ) + ); }; const hasLineHeight = () => !! value?.typography?.lineHeight; const resetLineHeight = () => setLineHeight( undefined ); @@ -246,13 +237,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.letterSpacing ); const setLetterSpacing = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - letterSpacing: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'letterSpacing' ], newValue ) + ); }; const hasLetterSpacing = () => !! value?.typography?.letterSpacing; const resetLetterSpacing = () => setLetterSpacing( undefined ); @@ -261,13 +248,9 @@ export default function TypographyPanel( { const hasTextColumnsControl = useHasTextColumnsControl( settings ); const textColumns = decodeValue( inheritedValue?.typography?.textColumns ); const setTextColumns = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textColumns: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textColumns' ], newValue ) + ); }; const hasTextColumns = () => !! value?.typography?.textColumns; const resetTextColumns = () => setTextColumns( undefined ); @@ -278,13 +261,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.textTransform ); const setTextTransform = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textTransform: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textTransform' ], newValue ) + ); }; const hasTextTransform = () => !! value?.typography?.textTransform; const resetTextTransform = () => setTextTransform( undefined ); @@ -295,13 +274,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.textDecoration ); const setTextDecoration = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textDecoration: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textDecoration' ], newValue ) + ); }; const hasTextDecoration = () => !! value?.typography?.textDecoration; const resetTextDecoration = () => setTextDecoration( undefined ); diff --git a/packages/block-editor/src/hooks/test/utils.js b/packages/block-editor/src/hooks/test/utils.js index bfbced3195b7fb..a919fad575312e 100644 --- a/packages/block-editor/src/hooks/test/utils.js +++ b/packages/block-editor/src/hooks/test/utils.js @@ -7,113 +7,9 @@ import { applyFilters } from '@wordpress/hooks'; * Internal dependencies */ import '../anchor'; -import { immutableSet } from '../utils'; const noop = () => {}; -describe( 'immutableSet', () => { - describe( 'handling falsy values properly', () => { - it( 'should create a new object if `undefined` is passed', () => { - const result = immutableSet( undefined, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `null` is passed', () => { - const result = immutableSet( null, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `false` is passed', () => { - const result = immutableSet( false, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `0` is passed', () => { - const result = immutableSet( 0, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if an empty string is passed', () => { - const result = immutableSet( '', 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if a NaN is passed', () => { - const result = immutableSet( NaN, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - } ); - - describe( 'manages data assignment properly', () => { - it( 'assigns value properly when it does not exist', () => { - const result = immutableSet( {}, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'overrides existing values', () => { - const result = immutableSet( { test: 1 }, 'test', 2 ); - - expect( result ).toEqual( { test: 2 } ); - } ); - - describe( 'with array notation access', () => { - it( 'assigns values at deeper levels', () => { - const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 ); - - expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); - } ); - - it( 'overrides existing values at deeper levels', () => { - const result = immutableSet( - { foo: { bar: { baz: 1 } } }, - [ 'foo', 'bar', 'baz' ], - 5 - ); - - expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); - } ); - - it( 'keeps other properties intact', () => { - const result = immutableSet( - { foo: { bar: { baz: 1 } } }, - [ 'foo', 'bar', 'test' ], - 5 - ); - - expect( result ).toEqual( { - foo: { bar: { baz: 1, test: 5 } }, - } ); - } ); - } ); - } ); - - describe( 'does not mutate the original object', () => { - it( 'clones the object at the first level', () => { - const input = {}; - const result = immutableSet( input, 'test', 1 ); - - expect( result ).not.toBe( input ); - } ); - - it( 'clones the object at deeper levels', () => { - const input = { foo: { bar: { baz: 1 } } }; - const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 ); - - expect( result ).not.toBe( input ); - expect( result.foo ).not.toBe( input.foo ); - expect( result.foo.bar ).not.toBe( input.foo.bar ); - expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz ); - } ); - } ); -} ); - describe( 'anchor', () => { const blockSettings = { save: noop, diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 8e702a3cede363..c7748ef85a7a0e 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -14,6 +14,7 @@ import { useMemo } from '@wordpress/element'; */ import { useSetting } from '../components'; import { useSettingsForBlockElement } from '../components/global-styles/hooks'; +import { immutableSet } from '../utils/object'; /** * Removed falsy values from nested object. @@ -37,76 +38,6 @@ export const cleanEmptyObject = ( object ) => { return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects; }; -/** - * Converts a path to an array of its fragments. - * Supports strings, numbers and arrays: - * - * 'foo' => [ 'foo' ] - * 2 => [ '2' ] - * [ 'foo', 'bar' ] => [ 'foo', 'bar' ] - * - * @param {string|number|Array} path Path - * @return {Array} Normalized path. - */ -function normalizePath( path ) { - if ( Array.isArray( path ) ) { - return path; - } else if ( typeof path === 'number' ) { - return [ path.toString() ]; - } - - return [ path ]; -} - -/** - * Clones an object. - * Non-object values are returned unchanged. - * - * @param {*} object Object to clone. - * @return {*} Cloned object, or original literal non-object value. - */ -function cloneObject( object ) { - if ( typeof object === 'object' ) { - return { - ...Object.fromEntries( - Object.entries( object ).map( ( [ key, value ] ) => [ - key, - cloneObject( value ), - ] ) - ), - }; - } - - return object; -} - -/** - * Perform an immutable set. - * Handles nullish initial values. - * Clones all nested objects in the specified object. - * - * @param {Object} object Object to set a value in. - * @param {number|string|Array} path Path in the object to modify. - * @param {*} value New value to set. - * @return {Object} Cloned object with the new value set. - */ -export function immutableSet( object, path, value ) { - const normalizedPath = normalizePath( path ); - const newObject = object ? cloneObject( object ) : {}; - - normalizedPath.reduce( ( acc, key, i ) => { - if ( acc[ key ] === undefined ) { - acc[ key ] = {}; - } - if ( i === normalizedPath.length - 1 ) { - acc[ key ] = value; - } - return acc[ key ]; - }, newObject ); - - return newObject; -} - export function transformStyles( activeSupports, migrationPaths, diff --git a/packages/block-editor/src/utils/object.js b/packages/block-editor/src/utils/object.js new file mode 100644 index 00000000000000..5c6afbc7ba4ff2 --- /dev/null +++ b/packages/block-editor/src/utils/object.js @@ -0,0 +1,69 @@ +/** + * Converts a path to an array of its fragments. + * Supports strings, numbers and arrays: + * + * 'foo' => [ 'foo' ] + * 2 => [ '2' ] + * [ 'foo', 'bar' ] => [ 'foo', 'bar' ] + * + * @param {string|number|Array} path Path + * @return {Array} Normalized path. + */ +function normalizePath( path ) { + if ( Array.isArray( path ) ) { + return path; + } else if ( typeof path === 'number' ) { + return [ path.toString() ]; + } + + return [ path ]; +} + +/** + * Clones an object. + * Non-object values are returned unchanged. + * + * @param {*} object Object to clone. + * @return {*} Cloned object, or original literal non-object value. + */ +function cloneObject( object ) { + if ( typeof object === 'object' ) { + return { + ...Object.fromEntries( + Object.entries( object ).map( ( [ key, value ] ) => [ + key, + cloneObject( value ), + ] ) + ), + }; + } + + return object; +} + +/** + * Perform an immutable set. + * Handles nullish initial values. + * Clones all nested objects in the specified object. + * + * @param {Object} object Object to set a value in. + * @param {number|string|Array} path Path in the object to modify. + * @param {*} value New value to set. + * @return {Object} Cloned object with the new value set. + */ +export function immutableSet( object, path, value ) { + const normalizedPath = normalizePath( path ); + const newObject = object ? cloneObject( object ) : {}; + + normalizedPath.reduce( ( acc, key, i ) => { + if ( acc[ key ] === undefined ) { + acc[ key ] = {}; + } + if ( i === normalizedPath.length - 1 ) { + acc[ key ] = value; + } + return acc[ key ]; + }, newObject ); + + return newObject; +} diff --git a/packages/block-editor/src/utils/test/object.js b/packages/block-editor/src/utils/test/object.js new file mode 100644 index 00000000000000..2fa0bbf2784de9 --- /dev/null +++ b/packages/block-editor/src/utils/test/object.js @@ -0,0 +1,107 @@ +/** + * Internal dependencies + */ +import { immutableSet } from '../utils'; + +describe( 'immutableSet', () => { + describe( 'handling falsy values properly', () => { + it( 'should create a new object if `undefined` is passed', () => { + const result = immutableSet( undefined, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `null` is passed', () => { + const result = immutableSet( null, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `false` is passed', () => { + const result = immutableSet( false, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `0` is passed', () => { + const result = immutableSet( 0, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if an empty string is passed', () => { + const result = immutableSet( '', 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if a NaN is passed', () => { + const result = immutableSet( NaN, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + } ); + + describe( 'manages data assignment properly', () => { + it( 'assigns value properly when it does not exist', () => { + const result = immutableSet( {}, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'overrides existing values', () => { + const result = immutableSet( { test: 1 }, 'test', 2 ); + + expect( result ).toEqual( { test: 2 } ); + } ); + + describe( 'with array notation access', () => { + it( 'assigns values at deeper levels', () => { + const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 ); + + expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); + } ); + + it( 'overrides existing values at deeper levels', () => { + const result = immutableSet( + { foo: { bar: { baz: 1 } } }, + [ 'foo', 'bar', 'baz' ], + 5 + ); + + expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); + } ); + + it( 'keeps other properties intact', () => { + const result = immutableSet( + { foo: { bar: { baz: 1 } } }, + [ 'foo', 'bar', 'test' ], + 5 + ); + + expect( result ).toEqual( { + foo: { bar: { baz: 1, test: 5 } }, + } ); + } ); + } ); + } ); + + describe( 'does not mutate the original object', () => { + it( 'clones the object at the first level', () => { + const input = {}; + const result = immutableSet( input, 'test', 1 ); + + expect( result ).not.toBe( input ); + } ); + + it( 'clones the object at deeper levels', () => { + const input = { foo: { bar: { baz: 1 } } }; + const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 ); + + expect( result ).not.toBe( input ); + expect( result.foo ).not.toBe( input.foo ); + expect( result.foo.bar ).not.toBe( input.foo.bar ); + expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz ); + } ); + } ); +} ); From 565094d92f0038dcd8c04b5d75cce1a9ae706cae Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 27 Mar 2023 10:57:41 +0100 Subject: [PATCH 2/2] Fix unit tests --- packages/block-editor/src/utils/test/object.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/utils/test/object.js b/packages/block-editor/src/utils/test/object.js index 2fa0bbf2784de9..be5360064ac08b 100644 --- a/packages/block-editor/src/utils/test/object.js +++ b/packages/block-editor/src/utils/test/object.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { immutableSet } from '../utils'; +import { immutableSet } from '../object'; describe( 'immutableSet', () => { describe( 'handling falsy values properly', () => {