@elementor/editor-canvas 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +17 -0
  3. package/dist/index.js +275 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +275 -0
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +10 -5
  8. package/src/__tests__/init-styles-renderer.test.ts +69 -0
  9. package/src/init-styles-renderer.ts +43 -0
  10. package/src/init.tsx +3 -0
  11. package/src/styles-renderer/__tests__/__mocks__/style-definitions.ts +171 -0
  12. package/src/styles-renderer/__tests__/enqueue-used-fonts.test.ts +54 -0
  13. package/src/styles-renderer/__tests__/index.test.ts +132 -0
  14. package/src/styles-renderer/enqueue-used-fonts.ts +21 -0
  15. package/src/styles-renderer/index.ts +2 -0
  16. package/src/styles-renderer/render.ts +112 -0
  17. package/src/styles-renderer/transform.ts +34 -0
  18. package/src/styles-renderer/transformers/__tests__/background-overlay-transformer.test.ts +46 -0
  19. package/src/styles-renderer/transformers/__tests__/create-combine-array-transformer.test.ts +61 -0
  20. package/src/styles-renderer/transformers/__tests__/linked-dimensions-transformer.test.ts +34 -0
  21. package/src/styles-renderer/transformers/__tests__/shadow-transformer.test.ts +127 -0
  22. package/src/styles-renderer/transformers/__tests__/size-transformer.test.ts +37 -0
  23. package/src/styles-renderer/transformers/__tests__/stroke-transformer.test.ts +59 -0
  24. package/src/styles-renderer/transformers/background-overlay-transformer.ts +13 -0
  25. package/src/styles-renderer/transformers/border-radius-transformer.ts +20 -0
  26. package/src/styles-renderer/transformers/border-width-transformer.ts +15 -0
  27. package/src/styles-renderer/transformers/color-transformer.ts +11 -0
  28. package/src/styles-renderer/transformers/create-combine-array-transformer.ts +20 -0
  29. package/src/styles-renderer/transformers/index.ts +22 -0
  30. package/src/styles-renderer/transformers/linked-dimensions-transformer.ts +15 -0
  31. package/src/styles-renderer/transformers/shadow-transformer.ts +22 -0
  32. package/src/styles-renderer/transformers/size-transformer.ts +11 -0
  33. package/src/styles-renderer/transformers/stroke-transformer.ts +11 -0
  34. package/src/styles-renderer/types.ts +9 -0
  35. package/src/sync/enqueue-font.ts +7 -0
  36. package/src/sync/get-canvas-iframe-body.ts +7 -0
  37. package/src/sync/types.ts +10 -0
@@ -0,0 +1,171 @@
1
+ import { type BreakpointsMap } from '@elementor/editor-responsive';
2
+ import { type StyleDefinition } from '@elementor/editor-styles';
3
+
4
+ export const singleVariantStyleMock = {
5
+ id: `single-variants-style`,
6
+ type: 'class',
7
+ variants: [
8
+ {
9
+ meta: {
10
+ breakpoint: null,
11
+ state: null,
12
+ },
13
+ props: {
14
+ fontSize: '24px',
15
+ fontWeight: 'bold',
16
+ },
17
+ },
18
+ ],
19
+ } as unknown as StyleDefinition;
20
+
21
+ export const breakpointsStyleMock = {
22
+ id: `breakpoint-style`,
23
+ type: 'class',
24
+ variants: [
25
+ {
26
+ meta: {
27
+ breakpoint: null,
28
+ state: null,
29
+ },
30
+ props: {
31
+ padding: '24px',
32
+ },
33
+ },
34
+ {
35
+ meta: {
36
+ breakpoint: 'mobile',
37
+ state: null,
38
+ },
39
+ props: {
40
+ padding: '4px',
41
+ },
42
+ },
43
+ {
44
+ meta: {
45
+ breakpoint: 'tablet',
46
+ state: null,
47
+ },
48
+ props: {
49
+ padding: '16px',
50
+ },
51
+ },
52
+ ],
53
+ } as unknown as StyleDefinition;
54
+
55
+ export const statesStyleMock = {
56
+ id: `breakpoint-style`,
57
+ type: 'class',
58
+ variants: [
59
+ {
60
+ meta: {
61
+ breakpoint: null,
62
+ state: null,
63
+ },
64
+ props: {
65
+ fontWeight: 'normal',
66
+ },
67
+ },
68
+ {
69
+ meta: {
70
+ breakpoint: null,
71
+ state: 'hover',
72
+ },
73
+ props: {
74
+ fontWeight: 'bold',
75
+ },
76
+ },
77
+ {
78
+ meta: {
79
+ breakpoint: null,
80
+ state: 'focus',
81
+ },
82
+ props: {
83
+ color: 'red',
84
+ },
85
+ },
86
+ ],
87
+ } as unknown as StyleDefinition;
88
+
89
+ export const transformableStyleMock = {
90
+ id: `transformable-style`,
91
+ type: 'class',
92
+ variants: [
93
+ {
94
+ meta: {
95
+ breakpoint: null,
96
+ state: null,
97
+ },
98
+ props: {
99
+ fontSize: {
100
+ $$type: 'size',
101
+ value: {
102
+ unit: 'px',
103
+ size: 24,
104
+ },
105
+ },
106
+ },
107
+ },
108
+ ],
109
+ } as unknown as StyleDefinition;
110
+
111
+ export const multiVariantsStyleMock = {
112
+ id: `multi-variant-style`,
113
+ type: 'class',
114
+ variants: [
115
+ {
116
+ meta: {
117
+ breakpoint: null,
118
+ state: null,
119
+ },
120
+ props: {
121
+ fontSize: {
122
+ $$type: 'size',
123
+ value: {
124
+ unit: 'px',
125
+ size: 24,
126
+ },
127
+ },
128
+ fontWeight: 'normal',
129
+ },
130
+ },
131
+ {
132
+ meta: {
133
+ breakpoint: 'mobile',
134
+ state: null,
135
+ },
136
+ props: {
137
+ fontSize: {
138
+ $$type: 'size',
139
+ value: {
140
+ unit: 'px',
141
+ size: 20,
142
+ },
143
+ },
144
+ },
145
+ },
146
+ {
147
+ meta: {
148
+ breakpoint: null,
149
+ state: 'hover',
150
+ },
151
+ props: {
152
+ fontWeight: 'bold',
153
+ },
154
+ },
155
+ ],
156
+ } as unknown as StyleDefinition;
157
+
158
+ export const mockBreakpoints = {
159
+ tablet: {
160
+ id: 'tablet',
161
+ width: 992,
162
+ label: 'Tablet Portrait',
163
+ type: 'max-width',
164
+ },
165
+ mobile: {
166
+ id: 'mobile',
167
+ width: 768,
168
+ label: 'Mobile Portrait',
169
+ type: 'max-width',
170
+ },
171
+ } as BreakpointsMap;
@@ -0,0 +1,54 @@
1
+ import { type StyleDefinition } from '@elementor/editor-styles';
2
+
3
+ import { enqueueFont } from '../../sync/enqueue-font';
4
+ import enqueueUsedFonts from '../enqueue-used-fonts';
5
+
6
+ jest.mock( '../../sync/enqueue-font' );
7
+
8
+ describe( 'enqueueUsedFonts', () => {
9
+ it( 'should run enqueueFont for each font family found in the style defs', () => {
10
+ // Arrange.
11
+ jest.mocked( enqueueFont ).mockImplementation( () => {} );
12
+
13
+ const styles = [
14
+ {
15
+ id: '1',
16
+ type: 'class',
17
+ variants: [
18
+ {
19
+ meta: {
20
+ breakpoint: null,
21
+ state: null,
22
+ },
23
+ props: {
24
+ 'font-family': 'Open Sans',
25
+ },
26
+ },
27
+ ],
28
+ },
29
+ {
30
+ id: '2',
31
+ type: 'class',
32
+ variants: [
33
+ {
34
+ meta: {
35
+ breakpoint: null,
36
+ state: 'hover',
37
+ },
38
+ props: {
39
+ 'font-family': 'Roboto',
40
+ },
41
+ },
42
+ ],
43
+ },
44
+ ] as unknown as StyleDefinition[];
45
+
46
+ // Act.
47
+ enqueueUsedFonts( styles );
48
+
49
+ // Assert.
50
+ expect( enqueueFont ).toHaveBeenCalledTimes( 2 );
51
+ expect( enqueueFont ).toHaveBeenCalledWith( 'Open Sans' );
52
+ expect( enqueueFont ).toHaveBeenCalledWith( 'Roboto' );
53
+ } );
54
+ } );
@@ -0,0 +1,132 @@
1
+ import { transformers } from '../index';
2
+ import createStyles from '../render';
3
+ import {
4
+ breakpointsStyleMock,
5
+ mockBreakpoints,
6
+ multiVariantsStyleMock,
7
+ singleVariantStyleMock,
8
+ statesStyleMock,
9
+ transformableStyleMock,
10
+ } from './__mocks__/style-definitions';
11
+
12
+ describe( 'style renderer', () => {
13
+ it( 'should return a string wrapped with style tag or without', () => {
14
+ // Arrange.
15
+ const styleRule = `.${ singleVariantStyleMock.id }{font-size:24px;font-weight:bold}`;
16
+
17
+ // Act.
18
+ const cssString = createStyles( {
19
+ styles: [ singleVariantStyleMock ],
20
+ transformers: {},
21
+ breakpoints: mockBreakpoints,
22
+ } );
23
+
24
+ // Assert.
25
+ expect( cssString ).toEqual( `<style data-style-id="${ singleVariantStyleMock.id }">${ styleRule }</style>` );
26
+ } );
27
+
28
+ it( 'should support breakpoints using media queries', () => {
29
+ // Arrange.
30
+ const className = `.${ breakpointsStyleMock.id }`;
31
+ const defaultStyleRule = `${ className }{padding:24px}`;
32
+ const smStyleRule = `@media(max-width:768px){${ className }{padding:4px}}`;
33
+ const mdStyleRule = `@media(max-width:992px){${ className }{padding:16px}}`;
34
+
35
+ // Act.
36
+ const cssString = createStyles( {
37
+ styles: [ breakpointsStyleMock ],
38
+ transformers: {},
39
+ breakpoints: mockBreakpoints,
40
+ } );
41
+
42
+ // Assert.
43
+ expect( cssString ).toEqual(
44
+ `<style data-style-id="${ breakpointsStyleMock.id }">${ [ defaultStyleRule, smStyleRule, mdStyleRule ].join(
45
+ ''
46
+ ) }</style>`
47
+ );
48
+ } );
49
+
50
+ it( 'should support states using pseudo selectors', () => {
51
+ // Arrange.
52
+ const className = `.${ statesStyleMock.id }`;
53
+ const defaultStyleRule = `${ className }{font-weight:normal}`;
54
+ const hoverStyleRule = `${ className }:hover{font-weight:bold}`;
55
+ const focusStyleRule = `${ className }:focus{color:red}`;
56
+
57
+ // Act.
58
+ const cssString = createStyles( {
59
+ styles: [ statesStyleMock ],
60
+ transformers: {},
61
+ breakpoints: mockBreakpoints,
62
+ } );
63
+
64
+ // Assert.
65
+ expect( cssString ).toEqual(
66
+ `<style data-style-id="${ breakpointsStyleMock.id }">${ [
67
+ defaultStyleRule,
68
+ hoverStyleRule,
69
+ focusStyleRule,
70
+ ].join( '' ) }</style>`
71
+ );
72
+ } );
73
+
74
+ it( 'should suppress non supported transformable values', () => {
75
+ // Arrange.
76
+ const defaultStyleRule = `.${ transformableStyleMock.id }{font-size:unset}`;
77
+
78
+ // Act.
79
+ const cssString = createStyles( {
80
+ styles: [ transformableStyleMock ],
81
+ transformers: {},
82
+ breakpoints: mockBreakpoints,
83
+ } );
84
+
85
+ // Assert.
86
+ expect( console ).toHaveErrored();
87
+ expect( cssString ).toEqual(
88
+ `<style data-style-id="${ transformableStyleMock.id }">${ defaultStyleRule }</style>`
89
+ );
90
+ } );
91
+
92
+ it( 'should support known transformable values', () => {
93
+ // Arrange.
94
+ const defaultStyleRule = `.${ transformableStyleMock.id }{font-size:24px}`;
95
+
96
+ // Act.
97
+ const cssString = createStyles( {
98
+ styles: [ transformableStyleMock ],
99
+ transformers: { size: transformers.size },
100
+ breakpoints: mockBreakpoints,
101
+ } );
102
+
103
+ // Assert.
104
+ expect( cssString ).toEqual(
105
+ `<style data-style-id="${ transformableStyleMock.id }">${ defaultStyleRule }</style>`
106
+ );
107
+ } );
108
+
109
+ it( 'should support multi variants of mixed variation types', () => {
110
+ // Arrange.
111
+ const className = `.${ multiVariantsStyleMock.id }`;
112
+ const defaultStyleRule = `${ className }{font-size:24px;font-weight:normal}`;
113
+ const smStyleRule = `@media(max-width:768px){${ className }{font-size:20px}}`;
114
+ const hoverStyleRule = `${ className }:hover{font-weight:bold}`;
115
+
116
+ // Act.
117
+ const cssString = createStyles( {
118
+ styles: [ multiVariantsStyleMock ],
119
+ transformers: { size: transformers.size },
120
+ breakpoints: mockBreakpoints,
121
+ } );
122
+
123
+ // Assert.
124
+ expect( cssString ).toEqual(
125
+ `<style data-style-id="${ multiVariantsStyleMock.id }">${ [
126
+ defaultStyleRule,
127
+ smStyleRule,
128
+ hoverStyleRule,
129
+ ].join( '' ) }</style>`
130
+ );
131
+ } );
132
+ } );
@@ -0,0 +1,21 @@
1
+ import { type StyleDefinition } from '@elementor/editor-styles';
2
+ import { ensureError } from '@elementor/utils';
3
+
4
+ import { enqueueFont } from '../sync/enqueue-font';
5
+
6
+ export default function enqueueUsedFonts( styles: StyleDefinition[] ) {
7
+ try {
8
+ styles.forEach( ( styleDef ) => {
9
+ Object.values( styleDef.variants ).forEach( ( variant ) => {
10
+ const fontFamily = variant.props[ 'font-family' ] ?? null;
11
+
12
+ if ( fontFamily && typeof fontFamily === 'string' ) {
13
+ enqueueFont( fontFamily );
14
+ }
15
+ } );
16
+ } );
17
+ } catch ( error: unknown ) {
18
+ // eslint-disable-next-line no-console
19
+ console.error( `Cannot enqueue font': ${ ensureError( error ).message }` );
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ export { default as transformers } from './transformers';
2
+ export { default as render } from './render';
@@ -0,0 +1,112 @@
1
+ import { type Props } from '@elementor/editor-props';
2
+ import { type Breakpoint, type BreakpointId, type BreakpointsMap } from '@elementor/editor-responsive';
3
+ import { type StyleDefinition, type StyleVariant } from '@elementor/editor-styles';
4
+ import { ensureError } from '@elementor/utils';
5
+
6
+ import { transformValue } from './transform';
7
+ import { type TransformersMap } from './types';
8
+
9
+ type RenderParams = {
10
+ transformers: TransformersMap;
11
+ styles: StyleDefinition[];
12
+ breakpoints: BreakpointsMap;
13
+ };
14
+
15
+ export default function render( { transformers, styles, breakpoints }: RenderParams ) {
16
+ const cssStyle: string[] = [];
17
+
18
+ try {
19
+ styles.forEach( ( styleDef ) => {
20
+ const style = renderStyle( styleDef, transformers, breakpoints );
21
+
22
+ cssStyle.push( wrapWithStyleElement( styleDef.id, style ) );
23
+ } );
24
+ } catch ( error: unknown ) {
25
+ // eslint-disable-next-line no-console
26
+ console.error( `Cannot render style': ${ ensureError( error ).message }` );
27
+ }
28
+
29
+ return cssStyle.join( '' );
30
+ }
31
+
32
+ function renderStyle( style: StyleDefinition, transformers: TransformersMap, breakpoints: BreakpointsMap ) {
33
+ const baseSelector = getBaseSelector( style );
34
+
35
+ if ( ! baseSelector ) {
36
+ return '';
37
+ }
38
+
39
+ const stylesheet: string[] = [];
40
+
41
+ Object.values( style.variants ).forEach( ( variant ) => {
42
+ const styleDeclaration = variantToStyleDeclaration( baseSelector, variant, transformers, breakpoints );
43
+
44
+ if ( styleDeclaration ) {
45
+ stylesheet.push( styleDeclaration );
46
+ }
47
+ } );
48
+
49
+ return stylesheet.join( '' );
50
+ }
51
+
52
+ function getBaseSelector( styleDef: StyleDefinition ) {
53
+ const map = {
54
+ class: '.',
55
+ };
56
+
57
+ return `${ map[ styleDef.type ] }${ styleDef.id }`;
58
+ }
59
+
60
+ function variantToStyleDeclaration(
61
+ baseSelector: string,
62
+ variant: StyleVariant,
63
+ transformers: TransformersMap,
64
+ breakpoints: BreakpointsMap
65
+ ) {
66
+ const css = propsToCss( variant.props, transformers );
67
+
68
+ if ( ! css ) {
69
+ return '';
70
+ }
71
+
72
+ const state = variant.meta.state ? `:${ variant.meta.state }` : '';
73
+ const selector = `${ baseSelector }${ state }`;
74
+
75
+ let styleDeclaration = `${ selector }{${ css }}`;
76
+
77
+ if ( variant.meta.breakpoint ) {
78
+ styleDeclaration = wrapWithMediaQuery( breakpoints, variant.meta.breakpoint, styleDeclaration );
79
+ }
80
+
81
+ return styleDeclaration;
82
+ }
83
+
84
+ function propsToCss( props: Props, transformers: TransformersMap ) {
85
+ return Object.entries( props )
86
+ .reduce< string[] >( ( acc, [ cssProp, cssValue ] ) => {
87
+ const prop = camelCaseToDash( cssProp );
88
+ const value = transformValue( cssValue, transformers );
89
+
90
+ acc.push( prop + ':' + value );
91
+
92
+ return acc;
93
+ }, [] )
94
+ .join( ';' );
95
+ }
96
+
97
+ function camelCaseToDash( str: string ) {
98
+ return str.replace( /([a-zA-Z])(?=[A-Z])/g, '$1-' ).toLowerCase();
99
+ }
100
+
101
+ function wrapWithMediaQuery( breakpoints: BreakpointsMap, breakpoint: BreakpointId, css: string ) {
102
+ const size = getBreakpointSize( breakpoints[ breakpoint ] );
103
+ return size ? `@media(${ size }){${ css }}` : css;
104
+ }
105
+
106
+ function getBreakpointSize( breakpoint: Breakpoint ) {
107
+ return breakpoint.type ? `${ breakpoint.type }:${ breakpoint.width }px` : null;
108
+ }
109
+
110
+ function wrapWithStyleElement( id: string, content: string ) {
111
+ return `<style data-style-id="${ id }">${ content }</style>`;
112
+ }
@@ -0,0 +1,34 @@
1
+ import { isTransformable, type PropValue } from '@elementor/editor-props';
2
+
3
+ import { type TransformersMap } from './types';
4
+
5
+ const FALLBACK_VALUE = 'unset';
6
+
7
+ export function transformValue( value: PropValue | undefined, transformers: TransformersMap ): PropValue {
8
+ if ( ! isTransformable( value ) ) {
9
+ return value;
10
+ }
11
+
12
+ const transformer = transformers[ value.$$type ];
13
+
14
+ if ( ! transformer ) {
15
+ // eslint-disable-next-line no-console
16
+ console.error( `Transformer not found for prop type '${ value.$$type }'` );
17
+
18
+ return FALLBACK_VALUE;
19
+ }
20
+
21
+ const transformedValue = transformer( value, {
22
+ transform: ( v ) => transformValue( v, transformers ),
23
+ } );
24
+
25
+ if ( transformedValue === undefined ) {
26
+ // eslint-disable-next-line no-console
27
+ console.error( `Transformer '${ value.$$type }' received unsupported value` );
28
+
29
+ return FALLBACK_VALUE;
30
+ }
31
+
32
+ // Transform recursively to support transformers that return transformable values.
33
+ return transformValue( transformedValue, transformers );
34
+ }
@@ -0,0 +1,46 @@
1
+ import backgroundOverlayTransformer from '../background-overlay-transformer';
2
+
3
+ describe( 'background-overlay-transformer', () => {
4
+ it( 'should return undefined if invalid', () => {
5
+ // Arrange.
6
+ const transform = jest.fn( ( item ) => item );
7
+ const initialData = {
8
+ $$type: 'background-overlay',
9
+ value: {
10
+ color: {
11
+ $$type: 'color',
12
+ value: null,
13
+ },
14
+ 'invalid-key': null,
15
+ },
16
+ };
17
+
18
+ // Act.
19
+ const result = backgroundOverlayTransformer( initialData, { transform } );
20
+
21
+ expect( result ).toBe( undefined );
22
+ expect( transform ).not.toHaveBeenCalled();
23
+ } );
24
+
25
+ it( 'should transform valid color input to linear gradient string', () => {
26
+ const transform = jest.fn( ( item ) =>
27
+ item?.value.size ? `${ item.value.size }${ item.value.unit }` : item?.value
28
+ );
29
+
30
+ const prop = {
31
+ $$type: 'background-overlay',
32
+ value: {
33
+ color: {
34
+ $$type: 'color',
35
+ value: 'rgba(0, 0, 0, 0.2)',
36
+ },
37
+ },
38
+ };
39
+
40
+ // Act.
41
+ const result = backgroundOverlayTransformer( prop, { transform } );
42
+
43
+ // Assert.
44
+ expect( result ).toBe( `linear-gradient( rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2) )` );
45
+ } );
46
+ } );
@@ -0,0 +1,61 @@
1
+ import { type PropValue } from '@elementor/editor-props';
2
+
3
+ import createCombineArrayTransformer from '../create-combine-array-transformer';
4
+
5
+ describe( 'combine-array-transformer', () => {
6
+ it.each( [
7
+ 'string',
8
+ { $$type: 'object', value: { unit: 'px', size: 1 } },
9
+ { $$type: 'string', value: 'rgba(0, 0, 0, 1)' },
10
+ { $$type: 'number', value: 1 },
11
+ { $$type: 'boolean', value: true },
12
+ ] )( 'should return undefined if value is not an array', ( data ) => {
13
+ // Arrange.
14
+ const transform = jest.fn();
15
+ const transformer = createCombineArrayTransformer( ' ' );
16
+
17
+ // Act.
18
+ const result = transformer( data, { transform } );
19
+
20
+ // Assert.
21
+ expect( result ).toBe( undefined );
22
+ expect( transform ).not.toHaveBeenCalled();
23
+ } );
24
+
25
+ it( 'should return empty string if value is an empty array', () => {
26
+ // Arrange.
27
+ const transform = jest.fn();
28
+ const transformer = createCombineArrayTransformer( ' ' );
29
+ const data = {
30
+ $$type: 'array',
31
+ value: [],
32
+ };
33
+
34
+ // Act.
35
+ const result = transformer( data, { transform } );
36
+
37
+ // Assert.
38
+ expect( result ).toBe( '' );
39
+ } );
40
+
41
+ it( 'should combine array of strings with given delimiter', () => {
42
+ // Arrange.
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const transform = ( item: PropValue ) => ( item as any ).value;
45
+ const transformer = createCombineArrayTransformer( ', ' );
46
+ const data = {
47
+ $$type: 'array',
48
+ value: [
49
+ { $$type: 'string', value: 'a' },
50
+ { $$type: 'string', value: 'b' },
51
+ { $$type: 'string', value: 'c' },
52
+ ],
53
+ };
54
+
55
+ // Act.
56
+ const result = transformer( data, { transform } );
57
+
58
+ // Assert.
59
+ expect( result ).toBe( 'a, b, c' );
60
+ } );
61
+ } );
@@ -0,0 +1,34 @@
1
+ import LinkedDimensionsTransformer from '../linked-dimensions-transformer';
2
+
3
+ describe( 'linked-dimensions-transformer', () => {
4
+ it.each( [
5
+ { value: { isLinked: true, top: '10', right: '10', bottom: '10', 'invalid-left-key': '10' } },
6
+ { value: { 'invalid-key': '10' } },
7
+ ] )( 'should return undefined if invalid', async ( { value } ) => {
8
+ // Arrange.
9
+ const transform = jest.fn( ( item ) => item );
10
+ const initialData = { $$type: 'linked-dimensions', value };
11
+
12
+ // Act.
13
+ const result = LinkedDimensionsTransformer( initialData, { transform } );
14
+
15
+ // Assert.
16
+ expect( result ).toBe( undefined );
17
+ expect( transform ).not.toHaveBeenCalled();
18
+ } );
19
+
20
+ it( 'should transform linked-dimensions to a string', () => {
21
+ // Arrange.
22
+ const transform = jest.fn( ( value ) => value );
23
+ const linkedDimensions = { isLinked: false, top: undefined, right: '10', bottom: undefined, left: '10' };
24
+
25
+ // Act.
26
+ const result = LinkedDimensionsTransformer(
27
+ { $$type: 'linked-dimensions', value: linkedDimensions },
28
+ { transform }
29
+ );
30
+
31
+ // Assert.
32
+ expect( result ).toBe( 'unset 10 unset 10' );
33
+ } );
34
+ } );