@elementor/editor-canvas 3.35.0-472 → 3.35.0-474

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createMockElement,
3
+ createMockPropType,
3
4
  createMockStyleDefinitionWithVariants,
4
5
  dispatchCommandBefore,
5
6
  mockHistoryManager,
@@ -7,37 +8,56 @@ import {
7
8
  import {
8
9
  createElementStyle,
9
10
  deleteElementStyle,
11
+ getContainer,
12
+ getElementSetting,
10
13
  getElementStyles,
14
+ getWidgetsCache,
15
+ updateElementSettings,
11
16
  updateElementStyle,
12
17
  } from '@elementor/editor-elements';
18
+ import { classesPropTypeUtil } from '@elementor/editor-props';
13
19
  import { type StyleDefinition } from '@elementor/editor-styles';
14
20
  import { ELEMENTS_STYLES_RESERVED_LABEL } from '@elementor/editor-styles-repository';
15
21
 
16
22
  import { initPasteStyleCommand } from '../paste-style';
17
- import { getClassesProp, getClipboardElements, isAtomicWidget } from '../utils';
23
+ import { getClipboardElements } from '../utils';
18
24
 
19
25
  jest.mock( '@elementor/editor-elements' );
20
26
  jest.mock( '../utils', () => ( {
21
27
  ...jest.requireActual( '../utils' ),
22
28
  getClipboardElements: jest.fn(),
23
- getClassesProp: jest.fn(),
24
- isAtomicWidget: jest.fn(),
25
29
  } ) );
26
30
  jest.mock( '@elementor/editor-v1-adapters', () => ( {
27
31
  ...jest.requireActual( '@elementor/editor-v1-adapters' ),
28
32
  blockCommand: jest.fn(),
29
33
  } ) );
30
34
 
35
+ const ATOMIC_WIDGET_TYPE = 'atomic-widget';
36
+ const CLASSES_PROP_KEY = 'classes';
37
+
38
+ const mockWidgetSchema = {
39
+ [ ATOMIC_WIDGET_TYPE ]: {
40
+ elType: 'widget',
41
+ title: 'Test Atomic Widget',
42
+ controls: {},
43
+ atomic_controls: [],
44
+ atomic_props_schema: {
45
+ [ CLASSES_PROP_KEY ]: createMockPropType( { key: 'classes', kind: 'plain' } ),
46
+ },
47
+ atomic_style_states: [],
48
+ },
49
+ };
50
+
31
51
  describe( 'pasteStyles', () => {
32
52
  const historyMock = mockHistoryManager();
33
53
 
34
54
  beforeEach( () => {
35
55
  initPasteStyleCommand();
36
56
 
37
- jest.mocked( getClassesProp ).mockReturnValue( 'classes' );
57
+ jest.mocked( getWidgetsCache ).mockReturnValue( mockWidgetSchema );
38
58
  jest.mocked( createElementStyle ).mockReturnValue( 's-new' );
39
- jest.mocked( isAtomicWidget ).mockImplementation(
40
- ( container ) => container?.model.get( 'widgetType' ) === 'atomic-widget'
59
+ jest.mocked( getContainer ).mockImplementation( ( id ) =>
60
+ createMockElement( { model: { id, widgetType: ATOMIC_WIDGET_TYPE } } )
41
61
  );
42
62
 
43
63
  historyMock.beforeEach();
@@ -48,392 +68,419 @@ describe( 'pasteStyles', () => {
48
68
  jest.resetAllMocks();
49
69
  } );
50
70
 
51
- it( 'should override existing props, add any unset props to the final style, and ignore any original props that have no conflicts', () => {
52
- // Arrange.
53
- const container = createMockElement( {
54
- model: {
55
- id: 'test-container',
56
- widgetType: 'atomic-widget',
57
- },
58
- } );
59
-
60
- jest.mocked( getElementStyles ).mockReturnValue( {
61
- 's-1': createMockStyleDefinitionWithVariants( {
62
- id: 's-1',
63
- variants: [
64
- {
65
- meta: { breakpoint: null, state: null },
66
- props: { a: 0, b: 1, c: 2 },
67
- custom_css: null,
71
+ describe( 'Pasting local styles', () => {
72
+ it( 'should override existing props, add any unset props to the final style, and ignore any original props that have no conflicts', () => {
73
+ // Arrange.
74
+ const container = createMockElement( {
75
+ model: {
76
+ id: 'test-container',
77
+ widgetType: 'atomic-widget',
78
+ },
79
+ } );
80
+
81
+ jest.mocked( getElementStyles ).mockReturnValue( {
82
+ 's-1': createMockStyleDefinitionWithVariants( {
83
+ id: 's-1',
84
+ variants: [
85
+ {
86
+ meta: { breakpoint: null, state: null },
87
+ props: { a: 0, b: 1, c: 2 },
88
+ custom_css: null,
89
+ },
90
+ {
91
+ meta: { breakpoint: null, state: 'hover' },
92
+ props: { a: 1, b: 2 },
93
+ custom_css: null,
94
+ },
95
+ ],
96
+ } ),
97
+ } );
98
+
99
+ jest.mocked( getClipboardElements ).mockReturnValue( [
100
+ {
101
+ id: 'test-1',
102
+ elType: 'test-widget',
103
+ styles: {
104
+ 's-2': createMockStyleDefinitionWithVariants( {
105
+ id: 's-2',
106
+ variants: [
107
+ {
108
+ meta: { breakpoint: null, state: null },
109
+ props: { a: 1, c: 2 },
110
+ custom_css: null,
111
+ },
112
+ {
113
+ meta: { breakpoint: 'tablet', state: 'hover' },
114
+ props: { a: 3, b: 4 },
115
+ custom_css: null,
116
+ },
117
+ ],
118
+ } ),
68
119
  },
120
+ },
121
+ ] );
122
+
123
+ // Act.
124
+ dispatchCommandBefore( 'document/elements/paste-style', { container } );
125
+
126
+ // Assert.
127
+ expect( updateElementStyle ).toHaveBeenCalledWith( {
128
+ elementId: 'test-container',
129
+ styleId: 's-1',
130
+ meta: {
131
+ breakpoint: null,
132
+ state: null,
133
+ },
134
+ custom_css: null,
135
+ props: {
136
+ a: 1,
137
+ c: 2,
138
+ },
139
+ } );
140
+
141
+ expect( updateElementStyle ).toHaveBeenCalledWith( {
142
+ elementId: 'test-container',
143
+ styleId: 's-1',
144
+ meta: {
145
+ breakpoint: 'tablet',
146
+ state: 'hover',
147
+ },
148
+ custom_css: null,
149
+ props: {
150
+ a: 3,
151
+ b: 4,
152
+ },
153
+ } );
154
+
155
+ // Act.
156
+ historyMock.instance.undo();
157
+
158
+ // Assert.
159
+ expect( createElementStyle ).toHaveBeenCalledWith( {
160
+ elementId: 'test-container',
161
+ styleId: 's-1',
162
+ label: ELEMENTS_STYLES_RESERVED_LABEL,
163
+ classesProp: 'classes',
164
+ meta: {
165
+ breakpoint: null,
166
+ state: null,
167
+ },
168
+ custom_css: null,
169
+ props: {
170
+ a: 0,
171
+ b: 1,
172
+ c: 2,
173
+ },
174
+ additionalVariants: [
69
175
  {
70
176
  meta: { breakpoint: null, state: 'hover' },
71
177
  props: { a: 1, b: 2 },
72
178
  custom_css: null,
73
179
  },
74
180
  ],
75
- } ),
76
- } );
181
+ } );
77
182
 
78
- jest.mocked( getClipboardElements ).mockReturnValue( [
79
- {
80
- id: 'test-1',
81
- elType: 'test-widget',
82
- styles: {
83
- 's-2': createMockStyleDefinitionWithVariants( {
84
- id: 's-2',
85
- variants: [
86
- {
87
- meta: { breakpoint: null, state: null },
88
- props: { a: 1, c: 2 },
89
- custom_css: null,
90
- },
91
- {
92
- meta: { breakpoint: 'tablet', state: 'hover' },
93
- props: { a: 3, b: 4 },
94
- custom_css: null,
95
- },
96
- ],
97
- } ),
98
- },
99
- },
100
- ] );
101
-
102
- // Act.
103
- dispatchCommandBefore( 'document/elements/paste-style', { container } );
104
-
105
- // Assert.
106
- expect( updateElementStyle ).toHaveBeenCalledWith( {
107
- elementId: 'test-container',
108
- styleId: 's-1',
109
- meta: {
110
- breakpoint: null,
111
- state: null,
112
- },
113
- custom_css: null,
114
- props: {
115
- a: 1,
116
- c: 2,
117
- },
118
- } );
119
-
120
- expect( updateElementStyle ).toHaveBeenCalledWith( {
121
- elementId: 'test-container',
122
- styleId: 's-1',
123
- meta: {
124
- breakpoint: 'tablet',
125
- state: 'hover',
126
- },
127
- custom_css: null,
128
- props: {
129
- a: 3,
130
- b: 4,
131
- },
183
+ expect( deleteElementStyle ).not.toHaveBeenCalled();
184
+ expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
185
+ expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
132
186
  } );
133
187
 
134
- // Act.
135
- historyMock.instance.undo();
136
-
137
- // Assert.
138
- expect( createElementStyle ).toHaveBeenCalledWith( {
139
- elementId: 'test-container',
140
- styleId: 's-1',
141
- label: ELEMENTS_STYLES_RESERVED_LABEL,
142
- classesProp: 'classes',
143
- meta: {
144
- breakpoint: null,
145
- state: null,
146
- },
147
- custom_css: null,
148
- props: {
149
- a: 0,
150
- b: 1,
151
- c: 2,
152
- },
153
- additionalVariants: [
154
- {
155
- meta: { breakpoint: null, state: 'hover' },
156
- props: { a: 1, b: 2 },
157
- custom_css: null,
188
+ it( 'should create a new style if the container does not have its own local style', () => {
189
+ // Arrange.
190
+ const container = createMockElement( {
191
+ model: {
192
+ id: 'test-container',
193
+ widgetType: 'atomic-widget',
158
194
  },
159
- ],
160
- } );
195
+ } );
161
196
 
162
- expect( deleteElementStyle ).not.toHaveBeenCalled();
163
- expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
164
- expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
165
- } );
166
-
167
- it( 'should create a new style if the container does not have its own local style', () => {
168
- // Arrange.
169
- const container = createMockElement( {
170
- model: {
171
- id: 'test-container',
172
- widgetType: 'atomic-widget',
173
- },
174
- } );
197
+ jest.mocked( getElementStyles ).mockReturnValue( {} );
175
198
 
176
- jest.mocked( getElementStyles ).mockReturnValue( {} );
177
-
178
- jest.mocked( getClipboardElements ).mockReturnValue( [
179
- {
180
- id: 'test-1',
181
- elType: 'test-widget',
182
- styles: {
183
- 's-2': createMockStyleDefinitionWithVariants( {
184
- id: 's-1',
185
- variants: [
186
- {
187
- meta: { breakpoint: null, state: null },
188
- props: { a: 0, b: 1 },
189
- custom_css: null,
190
- },
191
- {
192
- meta: { breakpoint: null, state: 'hover' },
193
- props: { a: 1, b: 2 },
194
- custom_css: null,
195
- },
196
- ],
197
- } ),
198
- },
199
- },
200
- ] );
201
-
202
- // Act.
203
- dispatchCommandBefore( 'document/elements/paste-style', { container } );
204
-
205
- // Assert.
206
- expect( createElementStyle ).toHaveBeenCalledWith( {
207
- elementId: 'test-container',
208
- label: ELEMENTS_STYLES_RESERVED_LABEL,
209
- classesProp: 'classes',
210
- meta: {
211
- breakpoint: null,
212
- state: null,
213
- },
214
- props: {
215
- a: 0,
216
- b: 1,
217
- },
218
- custom_css: null,
219
- additionalVariants: [
199
+ jest.mocked( getClipboardElements ).mockReturnValue( [
220
200
  {
221
- meta: {
222
- breakpoint: null,
223
- state: 'hover',
224
- },
225
- custom_css: null,
226
- props: {
227
- a: 1,
228
- b: 2,
201
+ id: 'test-1',
202
+ elType: 'test-widget',
203
+ styles: {
204
+ 's-2': createMockStyleDefinitionWithVariants( {
205
+ id: 's-1',
206
+ variants: [
207
+ {
208
+ meta: { breakpoint: null, state: null },
209
+ props: { a: 0, b: 1 },
210
+ custom_css: null,
211
+ },
212
+ {
213
+ meta: { breakpoint: null, state: 'hover' },
214
+ props: { a: 1, b: 2 },
215
+ custom_css: null,
216
+ },
217
+ ],
218
+ } ),
229
219
  },
230
220
  },
231
- ],
232
- } );
233
-
234
- // Act.
235
- historyMock.instance.undo();
221
+ ] );
222
+
223
+ // Act.
224
+ dispatchCommandBefore( 'document/elements/paste-style', { container } );
225
+
226
+ // Assert.
227
+ expect( createElementStyle ).toHaveBeenCalledWith( {
228
+ elementId: 'test-container',
229
+ label: ELEMENTS_STYLES_RESERVED_LABEL,
230
+ classesProp: 'classes',
231
+ meta: {
232
+ breakpoint: null,
233
+ state: null,
234
+ },
235
+ props: {
236
+ a: 0,
237
+ b: 1,
238
+ },
239
+ custom_css: null,
240
+ additionalVariants: [
241
+ {
242
+ meta: {
243
+ breakpoint: null,
244
+ state: 'hover',
245
+ },
246
+ custom_css: null,
247
+ props: {
248
+ a: 1,
249
+ b: 2,
250
+ },
251
+ },
252
+ ],
253
+ } );
236
254
 
237
- // Assert.
238
- expect( deleteElementStyle ).toHaveBeenCalledWith( 'test-container', 's-new' );
255
+ // Act.
256
+ historyMock.instance.undo();
239
257
 
240
- expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
241
- } );
258
+ // Assert.
259
+ expect( deleteElementStyle ).toHaveBeenCalledWith( 'test-container', 's-new' );
242
260
 
243
- it( 'should not allow paste if the element is not atomic', () => {
244
- // Arrange.
245
- const container = createMockElement( {
246
- model: {
247
- id: 'test-container',
248
- styles: {},
249
- widgetType: 'non-atomic-widget',
250
- },
261
+ expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
251
262
  } );
252
- jest.mocked( getElementStyles ).mockReturnValue( {} );
253
263
 
254
- jest.mocked( getClipboardElements ).mockReturnValue( [
255
- {
256
- id: 'test-1',
257
- elType: 'test-widget',
258
- styles: {
259
- 's-2': createMockStyleDefinitionWithVariants( {
260
- id: 's-2',
261
- variants: [
262
- {
263
- meta: { breakpoint: null, state: null },
264
- props: { a: 1, c: 2 },
265
- custom_css: null,
266
- },
267
- {
268
- meta: { breakpoint: 'tablet', state: 'hover' },
269
- props: { a: 3, b: 4 },
270
- custom_css: null,
271
- },
272
- ],
273
- } ),
264
+ it( 'should not allow paste if the element is not atomic', () => {
265
+ // Arrange.
266
+ const container = createMockElement( {
267
+ model: {
268
+ id: 'test-container',
269
+ styles: {},
270
+ widgetType: 'non-atomic-widget',
274
271
  },
275
- },
276
- ] );
272
+ } );
273
+ jest.mocked( getElementStyles ).mockReturnValue( {} );
277
274
 
278
- // Act.
279
- dispatchCommandBefore( 'document/elements/paste-style', { container } );
275
+ jest.mocked( getClipboardElements ).mockReturnValue( [
276
+ {
277
+ id: 'test-1',
278
+ elType: 'test-widget',
279
+ styles: {
280
+ 's-2': createMockStyleDefinitionWithVariants( {
281
+ id: 's-2',
282
+ variants: [
283
+ {
284
+ meta: { breakpoint: null, state: null },
285
+ props: { a: 1, c: 2 },
286
+ custom_css: null,
287
+ },
288
+ {
289
+ meta: { breakpoint: 'tablet', state: 'hover' },
290
+ props: { a: 3, b: 4 },
291
+ custom_css: null,
292
+ },
293
+ ],
294
+ } ),
295
+ },
296
+ },
297
+ ] );
280
298
 
281
- // Assert.
282
- expect( createElementStyle ).not.toHaveBeenCalled();
283
- expect( updateElementStyle ).not.toHaveBeenCalled();
284
- expect( deleteElementStyle ).not.toHaveBeenCalled();
285
- } );
299
+ // Act.
300
+ dispatchCommandBefore( 'document/elements/paste-style', { container } );
286
301
 
287
- it( 'should take only one style def from the clipboard, regardless of how many containers and/or local styles are there', () => {
288
- // Arrange.
289
- const container = createMockElement( {
290
- model: {
291
- id: 'test-container',
292
- widgetType: 'atomic-widget',
293
- },
302
+ // Assert.
303
+ expect( createElementStyle ).not.toHaveBeenCalled();
304
+ expect( updateElementStyle ).not.toHaveBeenCalled();
305
+ expect( deleteElementStyle ).not.toHaveBeenCalled();
294
306
  } );
295
307
 
296
- jest.mocked( getElementStyles ).mockReturnValue( {
297
- 's-1': createMockStyleDefinitionWithVariants( {
298
- id: 's-1',
299
- variants: [
300
- {
301
- meta: { breakpoint: null, state: null },
302
- props: { a: 0, b: 1 },
303
- custom_css: null,
308
+ it( 'should take only one style def from the clipboard, regardless of how many containers and/or local styles are there', () => {
309
+ // Arrange.
310
+ const container = createMockElement( {
311
+ model: {
312
+ id: 'test-container',
313
+ widgetType: 'atomic-widget',
314
+ },
315
+ } );
316
+
317
+ jest.mocked( getElementStyles ).mockReturnValue( {
318
+ 's-1': createMockStyleDefinitionWithVariants( {
319
+ id: 's-1',
320
+ variants: [
321
+ {
322
+ meta: { breakpoint: null, state: null },
323
+ props: { a: 0, b: 1 },
324
+ custom_css: null,
325
+ },
326
+ {
327
+ meta: { breakpoint: null, state: 'hover' },
328
+ props: { a: 1, b: 2 },
329
+ custom_css: null,
330
+ },
331
+ ],
332
+ } ),
333
+ } );
334
+
335
+ jest.mocked( getClipboardElements ).mockReturnValue( [
336
+ {
337
+ id: 'test-1',
338
+ elType: 'test-widget',
339
+ styles: {
340
+ 's-2': createMockStyleDefinitionWithVariants( {
341
+ id: 's-2',
342
+ variants: [
343
+ {
344
+ meta: { breakpoint: null, state: null },
345
+ props: { a: 1, c: 2 },
346
+ custom_css: null,
347
+ },
348
+ ],
349
+ } ),
350
+ 's-3': createMockStyleDefinitionWithVariants( {
351
+ id: 's-3',
352
+ variants: [
353
+ {
354
+ meta: { breakpoint: null, state: null },
355
+ props: { a: 3, c: 4 },
356
+ custom_css: null,
357
+ },
358
+ ],
359
+ } ),
304
360
  },
305
- {
306
- meta: { breakpoint: null, state: 'hover' },
307
- props: { a: 1, b: 2 },
308
- custom_css: null,
361
+ },
362
+ {
363
+ id: 'test-2',
364
+ elType: 'test-widget',
365
+ styles: {
366
+ 's-4': createMockStyleDefinitionWithVariants( {
367
+ id: 's-4',
368
+ variants: [
369
+ {
370
+ meta: { breakpoint: null, state: null },
371
+ props: { a: 4, c: 5 },
372
+ custom_css: null,
373
+ },
374
+ ],
375
+ } ),
309
376
  },
310
- ],
311
- } ),
312
- } );
313
-
314
- jest.mocked( getClipboardElements ).mockReturnValue( [
315
- {
316
- id: 'test-1',
317
- elType: 'test-widget',
318
- styles: {
319
- 's-2': createMockStyleDefinitionWithVariants( {
320
- id: 's-2',
321
- variants: [
322
- {
323
- meta: { breakpoint: null, state: null },
324
- props: { a: 1, c: 2 },
325
- custom_css: null,
326
- },
327
- ],
328
- } ),
329
- 's-3': createMockStyleDefinitionWithVariants( {
330
- id: 's-3',
331
- variants: [
332
- {
333
- meta: { breakpoint: null, state: null },
334
- props: { a: 3, c: 4 },
335
- custom_css: null,
336
- },
337
- ],
338
- } ),
339
377
  },
340
- },
341
- {
342
- id: 'test-2',
343
- elType: 'test-widget',
344
- styles: {
345
- 's-4': createMockStyleDefinitionWithVariants( {
346
- id: 's-4',
347
- variants: [
348
- {
349
- meta: { breakpoint: null, state: null },
350
- props: { a: 4, c: 5 },
351
- custom_css: null,
352
- },
353
- ],
354
- } ),
378
+ ] );
379
+
380
+ // Act.
381
+ dispatchCommandBefore( 'document/elements/paste-style', { container } );
382
+
383
+ // Assert.
384
+ expect( updateElementStyle ).toHaveBeenCalledWith( {
385
+ elementId: 'test-container',
386
+ styleId: 's-1',
387
+ meta: {
388
+ breakpoint: null,
389
+ state: null,
355
390
  },
356
- },
357
- ] );
358
-
359
- // Act.
360
- dispatchCommandBefore( 'document/elements/paste-style', { container } );
361
-
362
- // Assert.
363
- expect( updateElementStyle ).toHaveBeenCalledWith( {
364
- elementId: 'test-container',
365
- styleId: 's-1',
366
- meta: {
367
- breakpoint: null,
368
- state: null,
369
- },
370
- props: {
371
- a: 1,
372
- c: 2,
373
- },
374
- custom_css: null,
375
- } );
376
-
377
- expect( updateElementStyle ).toHaveBeenCalledTimes( 1 );
378
- } );
391
+ props: {
392
+ a: 1,
393
+ c: 2,
394
+ },
395
+ custom_css: null,
396
+ } );
379
397
 
380
- it( 'should ignore the command if the clipboard element has no styles', () => {
381
- // Arrange.
382
- const container = createMockElement( {
383
- model: {
384
- id: 'test-container',
385
- widgetType: 'atomic-widget',
386
- },
398
+ expect( updateElementStyle ).toHaveBeenCalledTimes( 1 );
387
399
  } );
388
400
 
389
- jest.mocked( getClipboardElements ).mockReturnValue( [
390
- {
391
- id: 'test-1',
392
- elType: 'test-widget',
393
- styles: {},
394
- },
395
- ] );
401
+ it( 'should ignore the command if the clipboard element has no styles', () => {
402
+ // Arrange.
403
+ const container = createMockElement( {
404
+ model: {
405
+ id: 'test-container',
406
+ widgetType: 'atomic-widget',
407
+ },
408
+ } );
396
409
 
397
- // Act.
398
- dispatchCommandBefore( 'document/elements/paste-style', { container } );
410
+ jest.mocked( getClipboardElements ).mockReturnValue( [
411
+ {
412
+ id: 'test-1',
413
+ elType: 'test-widget',
414
+ styles: {},
415
+ },
416
+ ] );
399
417
 
400
- // Assert.
401
- expect( createElementStyle ).not.toHaveBeenCalled();
402
- expect( updateElementStyle ).not.toHaveBeenCalled();
403
- expect( deleteElementStyle ).not.toHaveBeenCalled();
404
- } );
418
+ // Act.
419
+ dispatchCommandBefore( 'document/elements/paste-style', { container } );
405
420
 
406
- it( 'should support multiple containers responsively - create/update styles, and ignore non atomic widgets', () => {
407
- // Arrange.
408
- const container1 = createMockElement( {
409
- model: {
410
- id: 'test-container-1',
411
- widgetType: 'atomic-widget',
412
- },
421
+ // Assert.
422
+ expect( createElementStyle ).not.toHaveBeenCalled();
423
+ expect( updateElementStyle ).not.toHaveBeenCalled();
424
+ expect( deleteElementStyle ).not.toHaveBeenCalled();
413
425
  } );
414
426
 
415
- const container2 = createMockElement( {
416
- model: {
417
- id: 'test-container-2',
418
- widgetType: 'atomic-widget',
419
- },
420
- } );
427
+ it( 'should support multiple containers responsively - create/update styles, and ignore non atomic widgets', () => {
428
+ // Arrange.
429
+ const container1 = createMockElement( {
430
+ model: {
431
+ id: 'test-container-1',
432
+ widgetType: 'atomic-widget',
433
+ },
434
+ } );
421
435
 
422
- const container3 = createMockElement( {
423
- model: {
424
- id: 'test-container-3',
425
- widgetType: 'non-atomic-widget',
426
- },
427
- } );
436
+ const container2 = createMockElement( {
437
+ model: {
438
+ id: 'test-container-2',
439
+ widgetType: 'atomic-widget',
440
+ },
441
+ } );
428
442
 
429
- jest.mocked( getElementStyles ).mockImplementation( ( containerId ) => {
430
- switch ( containerId ) {
431
- case 'test-container-1':
432
- return {};
433
- case 'test-container-2':
434
- return {
435
- 's-1': createMockStyleDefinitionWithVariants( {
436
- id: 's-1',
443
+ const container3 = createMockElement( {
444
+ model: {
445
+ id: 'test-container-3',
446
+ widgetType: 'non-atomic-widget',
447
+ },
448
+ } );
449
+
450
+ jest.mocked( getElementStyles ).mockImplementation( ( containerId ) => {
451
+ switch ( containerId ) {
452
+ case 'test-container-1':
453
+ return {};
454
+ case 'test-container-2':
455
+ return {
456
+ 's-1': createMockStyleDefinitionWithVariants( {
457
+ id: 's-1',
458
+ variants: [
459
+ {
460
+ meta: { breakpoint: null, state: null },
461
+ props: { a: 0, b: 1 },
462
+ custom_css: null,
463
+ },
464
+ {
465
+ meta: { breakpoint: null, state: 'hover' },
466
+ props: { a: 1, b: 2 },
467
+ custom_css: null,
468
+ },
469
+ ],
470
+ } ),
471
+ } as Record< string, StyleDefinition >;
472
+ default:
473
+ return null;
474
+ }
475
+ } );
476
+
477
+ jest.mocked( getClipboardElements ).mockReturnValue( [
478
+ {
479
+ id: 'test-1',
480
+ elType: 'test-widget',
481
+ styles: {
482
+ 's-2': createMockStyleDefinitionWithVariants( {
483
+ id: 's-2',
437
484
  variants: [
438
485
  {
439
486
  meta: { breakpoint: null, state: null },
@@ -447,138 +494,324 @@ describe( 'pasteStyles', () => {
447
494
  },
448
495
  ],
449
496
  } ),
450
- } as Record< string, StyleDefinition >;
451
- default:
452
- return null;
453
- }
454
- } );
455
-
456
- jest.mocked( getClipboardElements ).mockReturnValue( [
457
- {
458
- id: 'test-1',
459
- elType: 'test-widget',
460
- styles: {
461
- 's-2': createMockStyleDefinitionWithVariants( {
462
- id: 's-2',
463
- variants: [
464
- {
465
- meta: { breakpoint: null, state: null },
466
- props: { a: 0, b: 1 },
467
- custom_css: null,
468
- },
469
- {
470
- meta: { breakpoint: null, state: 'hover' },
471
- props: { a: 1, b: 2 },
472
- custom_css: null,
473
- },
474
- ],
475
- } ),
497
+ },
476
498
  },
477
- },
478
- ] );
499
+ ] );
500
+
501
+ // Act.
502
+ dispatchCommandBefore( 'document/elements/paste-style', {
503
+ containers: [ container1, container2, container3 ],
504
+ } );
505
+
506
+ // Assert.
507
+ expect( createElementStyle ).toHaveBeenCalledWith( {
508
+ elementId: 'test-container-1',
509
+ label: ELEMENTS_STYLES_RESERVED_LABEL,
510
+ classesProp: 'classes',
511
+ meta: {
512
+ breakpoint: null,
513
+ state: null,
514
+ },
515
+ props: {
516
+ a: 0,
517
+ b: 1,
518
+ },
519
+ custom_css: null,
520
+ additionalVariants: [
521
+ {
522
+ meta: {
523
+ breakpoint: null,
524
+ state: 'hover',
525
+ },
526
+ props: {
527
+ a: 1,
528
+ b: 2,
529
+ },
530
+ custom_css: null,
531
+ },
532
+ ],
533
+ } );
534
+
535
+ expect( updateElementStyle ).toHaveBeenCalledWith( {
536
+ elementId: 'test-container-2',
537
+ styleId: 's-1',
538
+ meta: {
539
+ breakpoint: null,
540
+ state: null,
541
+ },
542
+ props: {
543
+ a: 0,
544
+ b: 1,
545
+ },
546
+ custom_css: null,
547
+ } );
548
+
549
+ expect( updateElementStyle ).toHaveBeenCalledWith( {
550
+ elementId: 'test-container-2',
551
+ styleId: 's-1',
552
+ meta: {
553
+ breakpoint: null,
554
+ state: null,
555
+ },
556
+ props: {
557
+ a: 0,
558
+ b: 1,
559
+ },
560
+ custom_css: null,
561
+ } );
562
+
563
+ expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
564
+ expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
565
+
566
+ // Act.
567
+ historyMock.instance.undo();
568
+
569
+ // Assert.
570
+ expect( deleteElementStyle ).toHaveBeenCalledWith( 'test-container-1', 's-new' );
571
+
572
+ expect( createElementStyle ).toHaveBeenCalledWith( {
573
+ elementId: 'test-container-2',
574
+ label: ELEMENTS_STYLES_RESERVED_LABEL,
575
+ classesProp: 'classes',
576
+ styleId: 's-1',
577
+ meta: {
578
+ breakpoint: null,
579
+ state: null,
580
+ },
581
+ props: {
582
+ a: 0,
583
+ b: 1,
584
+ },
585
+ custom_css: null,
586
+ additionalVariants: [
587
+ {
588
+ meta: {
589
+ breakpoint: null,
590
+ state: 'hover',
591
+ },
592
+ custom_css: null,
593
+ props: {
594
+ a: 1,
595
+ b: 2,
596
+ },
597
+ },
598
+ ],
599
+ } );
479
600
 
480
- // Act.
481
- dispatchCommandBefore( 'document/elements/paste-style', {
482
- containers: [ container1, container2, container3 ],
601
+ expect( deleteElementStyle ).toHaveBeenCalledTimes( 1 );
602
+ expect( createElementStyle ).toHaveBeenCalledTimes( 2 );
603
+ expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
483
604
  } );
605
+ } );
484
606
 
485
- // Assert.
486
- expect( createElementStyle ).toHaveBeenCalledWith( {
487
- elementId: 'test-container-1',
488
- label: ELEMENTS_STYLES_RESERVED_LABEL,
489
- classesProp: 'classes',
490
- meta: {
491
- breakpoint: null,
492
- state: null,
493
- },
494
- props: {
495
- a: 0,
496
- b: 1,
497
- },
498
- custom_css: null,
499
- additionalVariants: [
607
+ describe( 'Pasting styles and classes', () => {
608
+ type SelectedElementState = {
609
+ id: string;
610
+ hasLocalStyle: boolean;
611
+ existingClasses: string[];
612
+ };
613
+
614
+ type ClipboardState = {
615
+ hasLocalStyle: boolean;
616
+ otherClasses: string[];
617
+ };
618
+
619
+ type PasteStylesTestCase = {
620
+ scenario: string;
621
+ clipboard: ClipboardState;
622
+ selectedElements: SelectedElementState[];
623
+ expectedCalls: {
624
+ createElementStyle: number;
625
+ updateElementStyle: number;
626
+ updateElementSettings: number;
627
+ };
628
+ expectedClasses?: Record< string, string[] >;
629
+ };
630
+
631
+ const CLIPBOARD_ELEMENT_ID = 'clipboard-element';
632
+ const CLIPBOARD_STYLE_ID = 's-clipboard';
633
+
634
+ const createLocalStyle = ( id: string ): StyleDefinition =>
635
+ createMockStyleDefinitionWithVariants( {
636
+ id,
637
+ variants: [ { meta: { breakpoint: null, state: null }, props: { color: 'red' }, custom_css: null } ],
638
+ } );
639
+
640
+ const setupTestMocks = ( selectedElements: SelectedElementState[], clipboard: ClipboardState ) => {
641
+ const containerMap = new Map< string, SelectedElementState >();
642
+ selectedElements.forEach( ( el ) => containerMap.set( el.id, el ) );
643
+
644
+ jest.mocked( getContainer ).mockImplementation( ( id ) => {
645
+ if ( id === CLIPBOARD_ELEMENT_ID ) {
646
+ return createMockElement( { model: { id, widgetType: ATOMIC_WIDGET_TYPE } } );
647
+ }
648
+ const config = containerMap.get( id );
649
+ if ( ! config ) {
650
+ return null;
651
+ }
652
+ return createMockElement( { model: { id: config.id, widgetType: ATOMIC_WIDGET_TYPE } } );
653
+ } );
654
+
655
+ jest.mocked( getElementStyles ).mockImplementation( ( elementId ) => {
656
+ const config = containerMap.get( elementId );
657
+ if ( ! config?.hasLocalStyle ) {
658
+ return {};
659
+ }
660
+ return { [ `s-${ elementId }` ]: createLocalStyle( `s-${ elementId }` ) };
661
+ } );
662
+
663
+ jest.mocked( getElementSetting ).mockImplementation( ( elementId, prop ) => {
664
+ if ( prop !== CLASSES_PROP_KEY ) {
665
+ return null;
666
+ }
667
+
668
+ if ( elementId === CLIPBOARD_ELEMENT_ID ) {
669
+ const allClasses = clipboard.hasLocalStyle
670
+ ? [ CLIPBOARD_STYLE_ID, ...clipboard.otherClasses ]
671
+ : clipboard.otherClasses;
672
+ return allClasses.length ? classesPropTypeUtil.create( allClasses ) : null;
673
+ }
674
+
675
+ const config = containerMap.get( elementId );
676
+ if ( ! config?.existingClasses.length ) {
677
+ return null;
678
+ }
679
+ return classesPropTypeUtil.create( config.existingClasses );
680
+ } );
681
+
682
+ jest.mocked( getClipboardElements ).mockReturnValue( [
500
683
  {
501
- meta: {
502
- breakpoint: null,
503
- state: 'hover',
504
- },
505
- props: {
506
- a: 1,
507
- b: 2,
508
- },
509
- custom_css: null,
684
+ id: CLIPBOARD_ELEMENT_ID,
685
+ elType: 'widget',
686
+ styles: clipboard.hasLocalStyle
687
+ ? { [ CLIPBOARD_STYLE_ID ]: createLocalStyle( CLIPBOARD_STYLE_ID ) }
688
+ : {},
510
689
  },
511
- ],
512
- } );
690
+ ] );
691
+ };
513
692
 
514
- expect( updateElementStyle ).toHaveBeenCalledWith( {
515
- elementId: 'test-container-2',
516
- styleId: 's-1',
517
- meta: {
518
- breakpoint: null,
519
- state: null,
693
+ const createContainersForDispatch = ( elements: SelectedElementState[] ) =>
694
+ elements.map( ( el ) => createMockElement( { model: { id: el.id, widgetType: ATOMIC_WIDGET_TYPE } } ) );
695
+
696
+ const testCases: PasteStylesTestCase[] = [
697
+ {
698
+ scenario: 'do nothing when clipboard has no style and no classes',
699
+ clipboard: { hasLocalStyle: false, otherClasses: [] },
700
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [] } ],
701
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 0, updateElementSettings: 0 },
702
+ },
703
+ {
704
+ scenario: 'do nothing when there are no selected elements',
705
+ clipboard: { hasLocalStyle: true, otherClasses: [ 'global-1' ] },
706
+ selectedElements: [],
707
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 0, updateElementSettings: 0 },
520
708
  },
521
- props: {
522
- a: 0,
523
- b: 1,
709
+ {
710
+ scenario: 'create style when clipboard has style and selected has none',
711
+ clipboard: { hasLocalStyle: true, otherClasses: [] },
712
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [] } ],
713
+ expectedCalls: { createElementStyle: 1, updateElementStyle: 0, updateElementSettings: 0 },
524
714
  },
525
- custom_css: null,
526
- } );
527
-
528
- expect( updateElementStyle ).toHaveBeenCalledWith( {
529
- elementId: 'test-container-2',
530
- styleId: 's-1',
531
- meta: {
532
- breakpoint: null,
533
- state: null,
715
+ {
716
+ scenario: 'update style when clipboard has style and selected also has style',
717
+ clipboard: { hasLocalStyle: true, otherClasses: [] },
718
+ selectedElements: [ { id: 'el-1', hasLocalStyle: true, existingClasses: [] } ],
719
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 1, updateElementSettings: 0 },
534
720
  },
535
- props: {
536
- a: 0,
537
- b: 1,
721
+ {
722
+ scenario: 'add classes when clipboard has only global classes',
723
+ clipboard: { hasLocalStyle: false, otherClasses: [ 'global-1', 'global-2' ] },
724
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [] } ],
725
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 0, updateElementSettings: 1 },
726
+ expectedClasses: { 'el-1': [ 'global-1', 'global-2' ] },
538
727
  },
539
- custom_css: null,
540
- } );
541
-
542
- expect( createElementStyle ).toHaveBeenCalledTimes( 1 );
543
- expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
544
-
545
- // Act.
546
- historyMock.instance.undo();
547
-
548
- // Assert.
549
- expect( deleteElementStyle ).toHaveBeenCalledWith( 'test-container-1', 's-new' );
550
-
551
- expect( createElementStyle ).toHaveBeenCalledWith( {
552
- elementId: 'test-container-2',
553
- label: ELEMENTS_STYLES_RESERVED_LABEL,
554
- classesProp: 'classes',
555
- styleId: 's-1',
556
- meta: {
557
- breakpoint: null,
558
- state: null,
728
+ {
729
+ scenario: 'merge classes when both clipboard and selected have classes',
730
+ clipboard: { hasLocalStyle: false, otherClasses: [ 'global-1', 'global-2' ] },
731
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [ 'existing-1' ] } ],
732
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 0, updateElementSettings: 1 },
733
+ expectedClasses: { 'el-1': [ 'global-1', 'global-2', 'existing-1' ] },
559
734
  },
560
- props: {
561
- a: 0,
562
- b: 1,
735
+ {
736
+ scenario: 'deduplicate classes when same class exists on both',
737
+ clipboard: { hasLocalStyle: false, otherClasses: [ 'shared-class', 'global-1' ] },
738
+ selectedElements: [
739
+ { id: 'el-1', hasLocalStyle: false, existingClasses: [ 'shared-class', 'existing-1' ] },
740
+ ],
741
+ expectedCalls: { createElementStyle: 0, updateElementStyle: 0, updateElementSettings: 1 },
742
+ expectedClasses: { 'el-1': [ 'shared-class', 'global-1', 'existing-1' ] },
563
743
  },
564
- custom_css: null,
565
- additionalVariants: [
566
- {
567
- meta: {
568
- breakpoint: null,
569
- state: 'hover',
570
- },
571
- custom_css: null,
572
- props: {
573
- a: 1,
574
- b: 2,
575
- },
744
+ {
745
+ scenario: 'paste style and classes together',
746
+ clipboard: { hasLocalStyle: true, otherClasses: [ 'global-1' ] },
747
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [] } ],
748
+ expectedCalls: { createElementStyle: 1, updateElementStyle: 0, updateElementSettings: 1 },
749
+ expectedClasses: { 'el-1': [ 'global-1' ] },
750
+ },
751
+ {
752
+ scenario: 'handle multiple selected elements with mixed states',
753
+ clipboard: { hasLocalStyle: true, otherClasses: [ 'global-1' ] },
754
+ selectedElements: [
755
+ { id: 'el-1', hasLocalStyle: false, existingClasses: [] },
756
+ { id: 'el-2', hasLocalStyle: true, existingClasses: [ 'existing-1' ] },
757
+ ],
758
+ expectedCalls: { createElementStyle: 1, updateElementStyle: 1, updateElementSettings: 2 },
759
+ expectedClasses: {
760
+ 'el-1': [ 'global-1' ],
761
+ 'el-2': [ 'global-1', 'existing-1' ],
576
762
  },
577
- ],
578
- } );
579
-
580
- expect( deleteElementStyle ).toHaveBeenCalledTimes( 1 );
581
- expect( createElementStyle ).toHaveBeenCalledTimes( 2 );
582
- expect( updateElementStyle ).toHaveBeenCalledTimes( 2 );
763
+ },
764
+ {
765
+ scenario: 'not update settings when clipboard has only local style class (no global classes)',
766
+ clipboard: { hasLocalStyle: true, otherClasses: [] },
767
+ selectedElements: [ { id: 'el-1', hasLocalStyle: false, existingClasses: [ 'existing-1' ] } ],
768
+ expectedCalls: { createElementStyle: 1, updateElementStyle: 0, updateElementSettings: 0 },
769
+ },
770
+ ];
771
+
772
+ it.each( testCases )(
773
+ 'should $scenario',
774
+ ( {
775
+ clipboard,
776
+ selectedElements,
777
+ expectedCalls: {
778
+ createElementStyle: expectedCreateElementStyle,
779
+ updateElementStyle: expectedUpdateElementStyle,
780
+ updateElementSettings: expectedUpdateElementSettings,
781
+ },
782
+ expectedClasses,
783
+ } ) => {
784
+ // Arrange.
785
+ setupTestMocks( selectedElements, clipboard );
786
+ const containers = createContainersForDispatch( selectedElements );
787
+
788
+ // Act.
789
+ dispatchCommandBefore( 'document/elements/paste-style', {
790
+ containers,
791
+ } );
792
+
793
+ // Assert.
794
+ expect( createElementStyle ).toHaveBeenCalledTimes( expectedCreateElementStyle );
795
+ expect( updateElementStyle ).toHaveBeenCalledTimes( expectedUpdateElementStyle );
796
+ expect( updateElementSettings ).toHaveBeenCalledTimes( expectedUpdateElementSettings );
797
+
798
+ selectedElements.forEach( ( { id } ) => {
799
+ if ( expectedClasses?.[ id ] ) {
800
+ expect( updateElementSettings ).toHaveBeenCalledWith( {
801
+ id,
802
+ props: {
803
+ [ CLASSES_PROP_KEY ]: classesPropTypeUtil.create( expectedClasses[ id ] ),
804
+ },
805
+ } );
806
+ } else {
807
+ expect( updateElementSettings ).not.toHaveBeenCalledWith(
808
+ expect.objectContaining( {
809
+ id,
810
+ } )
811
+ );
812
+ }
813
+ } );
814
+ }
815
+ );
583
816
  } );
584
817
  } );