1- import { beforeEach , describe , expect , test } from 'vitest' ;
1+ import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest' ;
22import { userEvent } from 'vitest/browser' ;
3+ import {
4+ cleanup as cleanupBrowser ,
5+ render as renderBrowser ,
6+ } from 'vitest-browser-react' ;
37import { screen , act , within , waitFor } from '@testing-library/react' ;
4- import { createBlock } from '@wordpress/blocks' ;
8+ import {
9+ createBlock ,
10+ getBlockTypes ,
11+ unregisterBlockType ,
12+ } from '@wordpress/blocks' ;
13+ import { createElement , useState } from '@wordpress/element' ;
14+ import { dispatch , select } from '@wordpress/data' ;
15+ import {
16+ store as richTextStore ,
17+ unregisterFormatType ,
18+ } from '@wordpress/rich-text' ;
19+ import {
20+ BlockInspector ,
21+ privateApis as blockEditorPrivateApis ,
22+ store as blockEditorStore ,
23+ } from '@wordpress/block-editor' ;
524import {
625 initializeEditor ,
726 selectBlock ,
@@ -12,6 +31,16 @@ import { registerCoreBlocks } from '@wordpress/block-library';
1231import '../../../../block-editor/src/components/block-popover/style.scss' ;
1332// eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports -- Browser fixtures need the styles WordPress normally enqueues.
1433import '../../../../components/src/popover/style.scss' ;
34+ import { unlock } from '../../lock-unlock' ;
35+ import { getMediaColor } from '../edit/color-utils' ;
36+
37+ vi . mock ( import ( '../edit/color-utils' ) , { spy : true } ) ;
38+
39+ const {
40+ ExperimentalBlockCanvas : BlockCanvas ,
41+ ExperimentalBlockEditorProvider : BlockEditorProvider ,
42+ openMediaEditorModalKey,
43+ } = unlock ( blockEditorPrivateApis ) ;
1544const defaultSettings = {
1645 __experimentalFeatures : {
1746 color : {
@@ -212,6 +241,147 @@ describe( 'Cover block', () => {
212241 } ) ;
213242 } ) ;
214243
244+ describe ( 'media editor Undo' , ( ) => {
245+ const originalUrl =
246+ 'data:image/svg+xml,' +
247+ encodeURIComponent (
248+ '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="black"/></svg>'
249+ ) ;
250+ const updatedImage = { id : 11 , url : `${ originalUrl } #edited` } ;
251+
252+ beforeEach ( ( ) => {
253+ vi . mocked ( getMediaColor ) . mockResolvedValue ( '#ffffff' ) ;
254+ } ) ;
255+
256+ afterEach ( async ( ) => {
257+ await cleanupBrowser ( ) ;
258+ getBlockTypes ( ) . forEach ( ( { name } ) =>
259+ unregisterBlockType ( name )
260+ ) ;
261+ select ( richTextStore )
262+ . getFormatTypes ( )
263+ . forEach ( ( { name } ) => unregisterFormatType ( name ) ) ;
264+ } ) ;
265+
266+ async function openMediaEditor ( initialAttributes = { } ) {
267+ const openModal = vi . fn ( ) ;
268+ const block = createBlock ( 'core/cover' , {
269+ id : 10 ,
270+ url : originalUrl ,
271+ backgroundType : 'image' ,
272+ sizeSlug : 'medium' ,
273+ overlayColor : 'black' ,
274+ isDark : true ,
275+ isUserOverlayColor : false ,
276+ ...initialAttributes ,
277+ } ) ;
278+ function TestEditor ( ) {
279+ const [ blocks , setBlocks ] = useState ( [ block ] ) ;
280+ return createElement (
281+ BlockEditorProvider ,
282+ {
283+ value : blocks ,
284+ onInput : setBlocks ,
285+ onChange : setBlocks ,
286+ useSubRegistry : false ,
287+ settings : {
288+ ...defaultSettings ,
289+ [ openMediaEditorModalKey ] : openModal ,
290+ } ,
291+ } ,
292+ createElement ( BlockInspector ) ,
293+ createElement ( BlockCanvas , {
294+ height : '100%' ,
295+ shouldIframe : false ,
296+ } )
297+ ) ;
298+ }
299+ await renderBrowser ( createElement ( TestEditor ) ) ;
300+ await userEvent . click ( screen . getByLabelText ( 'Block: Cover' ) ) ;
301+ await userEvent . click (
302+ screen . getByRole ( 'button' , { name : 'Edit image' } )
303+ ) ;
304+ const { clientId } = select ( blockEditorStore ) . getBlocks ( ) [ 0 ] ;
305+ return {
306+ ...openModal . mock . calls [ 0 ] [ 0 ] ,
307+ getAttributes : ( ) =>
308+ select ( blockEditorStore ) . getBlockAttributes ( clientId ) ,
309+ editAttributes : ( attributes ) =>
310+ dispatch ( blockEditorStore ) . updateBlockAttributes (
311+ clientId ,
312+ attributes
313+ ) ,
314+ } ;
315+ }
316+
317+ test ( 'restores the image size and automatic overlay without reverting unrelated edits' , async ( ) => {
318+ const { onUpdate, onUndo, getAttributes, editAttributes } =
319+ await openMediaEditor ( ) ;
320+ const previous = getAttributes ( ) ;
321+ await act ( ( ) => onUpdate ( updatedImage ) ) ;
322+ expect ( getAttributes ( ) ) . toMatchObject ( {
323+ id : 11 ,
324+ sizeSlug : 'full' ,
325+ overlayColor : 'white' ,
326+ isDark : false ,
327+ } ) ;
328+ await act ( ( ) => editAttributes ( { dimRatio : 75 } ) ) ;
329+
330+ act ( ( ) => onUndo ( ) ) ;
331+
332+ expect ( getAttributes ( ) ) . toEqual ( { ...previous , dimRatio : 75 } ) ;
333+ } ) ;
334+
335+ test ( 'preserves a custom overlay when undoing the media update' , async ( ) => {
336+ const { onUpdate, onUndo, getAttributes, editAttributes } =
337+ await openMediaEditor ( {
338+ overlayColor : undefined ,
339+ customOverlayColor : '#123456' ,
340+ isUserOverlayColor : true ,
341+ } ) ;
342+ await act ( ( ) => onUpdate ( updatedImage ) ) ;
343+ await act ( ( ) =>
344+ editAttributes ( { customOverlayColor : '#654321' } )
345+ ) ;
346+
347+ act ( ( ) => onUndo ( ) ) ;
348+
349+ expect ( getAttributes ( ) ) . toMatchObject ( {
350+ id : 10 ,
351+ url : originalUrl ,
352+ sizeSlug : 'medium' ,
353+ customOverlayColor : '#654321' ,
354+ isUserOverlayColor : true ,
355+ } ) ;
356+ } ) ;
357+
358+ test ( 'does not reapply a media update when Undo runs before its color loads' , async ( ) => {
359+ const { onUpdate, onUndo, getAttributes } = await openMediaEditor ( ) ;
360+ const previous = getAttributes ( ) ;
361+ let resolveColor ;
362+ vi . mocked ( getMediaColor ) . mockReturnValueOnce (
363+ new Promise ( ( resolve ) => {
364+ resolveColor = resolve ;
365+ } )
366+ ) ;
367+ let updatePromise ;
368+ act ( ( ) => {
369+ updatePromise = onUpdate ( updatedImage ) ;
370+ } ) ;
371+
372+ act ( ( ) => onUndo ( ) ) ;
373+ await act ( async ( ) => {
374+ resolveColor ( '#ffffff' ) ;
375+ await updatePromise ;
376+ } ) ;
377+
378+ expect ( getAttributes ( ) ) . toEqual ( previous ) ;
379+ expect (
380+ screen . getByRole ( 'button' , { name : 'Edit image' } )
381+ ) . toBeEnabled ( ) ;
382+ } ) ;
383+ } ) ;
384+
215385 describe ( 'Inspector controls' , ( ) => {
216386 describe ( 'Media settings' , ( ) => {
217387 test ( 'does not display media settings panel if url is not set' , async ( ) => {
0 commit comments