diff --git a/packages/block-library/src/cover/test/block-controls.js b/packages/block-library/src/cover/test/block-controls.js
deleted file mode 100644
index ea14dfb38d7b7..0000000000000
--- a/packages/block-library/src/cover/test/block-controls.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * External dependencies
- */
-import { render, screen, fireEvent } from '@testing-library/react';
-
-// Need to mock the BlockControls wrapper as this requires a slot to run
-// so can't be easily unit tested.
-jest.mock( '@wordpress/block-editor', () => ( {
- ...jest.requireActual( '@wordpress/block-editor' ),
- BlockControls: ( { children } ) =>
{ children }
,
-} ) );
-
-/**
- * Internal dependencies
- */
-import CoverBlockControls from '../edit/block-controls';
-
-const setAttributes = jest.fn();
-const onSelectMedia = jest.fn();
-
-const currentSettings = { hasInnerBlocks: true, url: undefined };
-const defaultAttributes = {
- contentPosition: undefined,
- id: 1,
- useFeaturedImage: false,
- dimRatio: 50,
- minHeight: 300,
- minHeightUnit: 'px',
-};
-const defaultProps = {
- attributes: defaultAttributes,
- currentSettings,
- setAttributes,
- onSelectMedia,
-};
-
-beforeEach( () => {
- setAttributes.mockClear();
- onSelectMedia.mockClear();
-} );
-
-describe( 'Cover block controls', () => {
- describe( 'Full height toggle', () => {
- test( 'displays toggle full height button toggled off if minHeight not 100vh', () => {
- render( );
- expect(
- screen.getByRole( 'button', {
- pressed: false,
- name: 'Toggle full height',
- } )
- ).toBeInTheDocument();
- } );
- test( 'sets minHeight attributes to 100vh when clicked', () => {
- render( );
- fireEvent.click( screen.getByLabelText( 'Toggle full height' ) );
- expect( setAttributes ).toHaveBeenCalledWith( {
- minHeight: 100,
- minHeightUnit: 'vh',
- } );
- } );
- } );
-} );
diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js
new file mode 100644
index 0000000000000..c2510dcb666b6
--- /dev/null
+++ b/packages/block-library/src/cover/test/edit.js
@@ -0,0 +1,324 @@
+/**
+ * External dependencies
+ */
+import { screen, fireEvent, act, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import {
+ initializeEditor,
+ selectBlock,
+} from 'test/integration/helpers/integration-test-editor';
+
+async function setup( attributes ) {
+ const testBlock = { name: 'core/cover', attributes };
+ return initializeEditor( testBlock );
+}
+
+async function createAndSelectBlock() {
+ await userEvent.click(
+ screen.getByRole( 'button', {
+ name: 'Color: Black',
+ } )
+ );
+ await userEvent.click(
+ screen.getByRole( 'button', {
+ name: 'Select Cover',
+ } )
+ );
+}
+
+describe( 'Cover block', () => {
+ describe( 'Editor canvas', () => {
+ test( 'shows placeholder if background image and color not set', async () => {
+ await setup();
+
+ expect(
+ screen.getByRole( 'group', {
+ name: 'To edit this block, you need permission to upload media.',
+ } )
+ ).toBeInTheDocument();
+ } );
+
+ test( 'can set overlay color using color picker on block placeholder', async () => {
+ const { container } = await setup();
+ const colorPicker = screen.getByRole( 'button', {
+ name: 'Color: Black',
+ } );
+ await userEvent.click( colorPicker );
+ const color = colorPicker.style.backgroundColor;
+ expect(
+ screen.queryByRole( 'group', {
+ name: 'To edit this block, you need permission to upload media.',
+ } )
+ ).not.toBeInTheDocument();
+
+ // eslint-disable-next-line testing-library/no-node-access
+ const overlay = container.getElementsByClassName(
+ 'wp-block-cover__background'
+ );
+ expect( overlay[ 0 ] ).toHaveStyle(
+ `background-color: ${ color }`
+ );
+ } );
+
+ test( 'can have the title edited', async () => {
+ await setup();
+
+ await userEvent.click(
+ screen.getByRole( 'button', {
+ name: 'Color: Black',
+ } )
+ );
+
+ const title = screen.getByLabelText( 'Empty block;', {
+ exact: false,
+ } );
+ await userEvent.click( title );
+ await userEvent.keyboard( 'abc' );
+ expect( title ).toHaveTextContent( 'abc' );
+ } );
+ } );
+
+ describe( 'Block toolbar', () => {
+ test( 'full height toggle sets minHeight style attribute to 100vh when clicked', async () => {
+ await setup();
+ await createAndSelectBlock();
+
+ expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveStyle(
+ 'min-height: 100vh;'
+ );
+
+ await userEvent.click(
+ screen.getByLabelText( 'Toggle full height' )
+ );
+
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveStyle(
+ 'min-height: 100vh;'
+ );
+ } );
+
+ test( 'content position button sets content position', async () => {
+ await setup();
+ await createAndSelectBlock();
+
+ await userEvent.click(
+ screen.getByLabelText( 'Change content position' )
+ );
+
+ expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass(
+ 'has-custom-content-position'
+ );
+
+ await act( async () =>
+ within( screen.getByRole( 'grid' ) )
+ .getByRole( 'gridcell', {
+ name: 'top left',
+ } )
+ .focus()
+ );
+
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
+ 'has-custom-content-position'
+ );
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
+ 'is-position-top-left'
+ );
+ } );
+ } );
+
+ describe( 'Inspector controls', () => {
+ describe( 'Media settings', () => {
+ test( 'does not display media settings panel if url is not set', async () => {
+ await setup();
+ expect(
+ screen.queryByRole( 'button', {
+ name: 'Media settings',
+ } )
+ ).not.toBeInTheDocument();
+ } );
+ test( 'displays media settings panel if url is set', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+
+ await selectBlock( 'Block: Cover' );
+ expect(
+ screen.getByRole( 'button', {
+ name: 'Media settings',
+ } )
+ ).toBeInTheDocument();
+ } );
+ } );
+
+ test( 'sets hasParallax attribute to true if fixed background toggled', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+ expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass(
+ 'has-parallax'
+ );
+ await selectBlock( 'Block: Cover' );
+ await userEvent.click(
+ screen.getByLabelText( 'Fixed background' )
+ );
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
+ 'has-parallax'
+ );
+ } );
+
+ test( 'sets isRepeated attribute to true if repeated background toggled', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+ expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass(
+ 'is-repeated'
+ );
+ await selectBlock( 'Block: Cover' );
+ await userEvent.click(
+ screen.getByLabelText( 'Repeated background' )
+ );
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass(
+ 'is-repeated'
+ );
+ } );
+
+ test( 'sets left focalPoint attribute when focal point values changed', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+
+ await selectBlock( 'Block: Cover' );
+ await userEvent.clear( screen.getByLabelText( 'Left' ) );
+ await userEvent.type( screen.getByLabelText( 'Left' ), '100' );
+
+ expect(
+ within( screen.getByLabelText( 'Block: Cover' ) ).getByRole(
+ 'img'
+ )
+ ).toHaveStyle( 'object-position: 100% 50%;' );
+ } );
+
+ test( 'sets alt attribute if text entered in alt text box', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+
+ await selectBlock( 'Block: Cover' );
+ await userEvent.type(
+ screen.getByLabelText( 'Alt text (alternative text)' ),
+ 'Me'
+ );
+ expect( screen.getByAltText( 'Me' ) ).toBeInTheDocument();
+ } );
+
+ test( 'clears media when clear media button clicked', async () => {
+ await setup( {
+ url: 'http://localhost/my-image.jpg',
+ } );
+
+ await selectBlock( 'Block: Cover' );
+ expect(
+ within( screen.getByLabelText( 'Block: Cover' ) ).getByRole(
+ 'img'
+ )
+ ).toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole( 'button', {
+ name: 'Clear Media',
+ } )
+ );
+ expect(
+ within( screen.queryByLabelText( 'Block: Cover' ) ).queryByRole(
+ 'img'
+ )
+ ).not.toBeInTheDocument();
+ } );
+
+ describe( 'Color panel', () => {
+ test( 'applies selected opacity to block when number control value changed', async () => {
+ const { container } = await setup();
+
+ await createAndSelectBlock();
+
+ // eslint-disable-next-line testing-library/no-node-access
+ const overlay = container.getElementsByClassName(
+ 'wp-block-cover__background'
+ );
+
+ expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' );
+
+ await userEvent.click(
+ screen.getByRole( 'tab', {
+ name: 'Styles',
+ } )
+ );
+
+ fireEvent.change(
+ screen.getByRole( 'spinbutton', {
+ name: 'Overlay opacity',
+ } ),
+ {
+ target: { value: '40' },
+ }
+ );
+
+ expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-40' );
+ } );
+
+ test( 'applies selected opacity to block when slider moved', async () => {
+ const { container } = await setup();
+
+ await createAndSelectBlock();
+
+ // eslint-disable-next-line testing-library/no-node-access
+ const overlay = container.getElementsByClassName(
+ 'wp-block-cover__background'
+ );
+
+ expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' );
+
+ await userEvent.click(
+ screen.getByRole( 'tab', {
+ name: 'Styles',
+ } )
+ );
+
+ fireEvent.change(
+ screen.getByRole( 'slider', {
+ name: 'Overlay opacity',
+ } ),
+ { target: { value: 30 } }
+ );
+
+ expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-30' );
+ } );
+ } );
+
+ describe( 'Dimensions panel', () => {
+ test( 'sets minHeight attribute when number control value changed', async () => {
+ await setup();
+ await createAndSelectBlock();
+ await userEvent.click(
+ screen.getByRole( 'tab', {
+ name: 'Styles',
+ } )
+ );
+ await userEvent.clear(
+ screen.getByLabelText( 'Minimum height of cover' )
+ );
+ await userEvent.type(
+ screen.getByLabelText( 'Minimum height of cover' ),
+ '300'
+ );
+
+ expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveStyle(
+ 'min-height: 300px;'
+ );
+ } );
+ } );
+ } );
+} );
diff --git a/packages/e2e-tests/specs/editor/blocks/cover.test.js b/packages/e2e-tests/specs/editor/blocks/cover.test.js
index 092a0276ba0f2..56ed8455aefa9 100644
--- a/packages/e2e-tests/specs/editor/blocks/cover.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/cover.test.js
@@ -42,30 +42,6 @@ describe( 'Cover', () => {
await createNewPost();
} );
- it( 'can set overlay color using color picker on block placeholder', async () => {
- await insertBlock( 'Cover' );
- // Get the first color option from the block placeholder's color picker.
- const colorPickerButton = await page.waitForSelector(
- '.wp-block-cover__placeholder-background-options .components-circular-option-picker__option-wrapper:first-child button'
- );
- // Get the RGB value of the picked color.
- const pickedColor = await colorPickerButton.evaluate(
- ( node ) => node.style.backgroundColor
- );
- // Create the block by clicking selected color button.
- await colorPickerButton.click();
- // Get the block's background dim element.
- const backgroundDim = await page.waitForSelector(
- '.wp-block-cover .has-background-dim'
- );
- // Get the RGB value of the background dim.
- const dimColor = await backgroundDim.evaluate(
- ( node ) => node.style.backgroundColor
- );
-
- expect( pickedColor ).toEqual( dimColor );
- } );
-
it( 'can set background image using image upload on block placeholder', async () => {
await insertBlock( 'Cover' );
// Create the block using uploaded image.
@@ -95,27 +71,6 @@ describe( 'Cover', () => {
expect( backgroundDimOpacity ).toBe( '0.5' );
} );
- it( 'can have the title edited', async () => {
- await insertBlock( 'Cover' );
- // Click first color option from the block placeholder's color picker.
- const colorPickerButton = await page.waitForSelector(
- '.wp-block-cover__placeholder-background-options .components-circular-option-picker__option-wrapper:first-child button'
- );
- await colorPickerButton.click();
- // Click the title placeholder to put the cursor inside.
- const coverTitle = await page.waitForSelector(
- '.wp-block-cover .wp-block-paragraph'
- );
- await coverTitle.click();
- // Type the title.
- await page.keyboard.type( 'foo' );
- const coverTitleText = await coverTitle.evaluate(
- ( el ) => el.innerText
- );
-
- expect( coverTitleText ).toEqual( 'foo' );
- } );
-
it( 'can be resized using drag & drop', async () => {
await insertBlock( 'Cover' );
// Close the inserter.
diff --git a/test/integration/helpers/integration-test-editor.js b/test/integration/helpers/integration-test-editor.js
new file mode 100644
index 0000000000000..6b954a895a51a
--- /dev/null
+++ b/test/integration/helpers/integration-test-editor.js
@@ -0,0 +1,125 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+import {
+ BlockEditorKeyboardShortcuts,
+ BlockEditorProvider,
+ BlockList,
+ BlockTools,
+ BlockInspector,
+ WritingFlow,
+ ObserveTyping,
+} from '@wordpress/block-editor';
+import { Popover, SlotFillProvider } from '@wordpress/components';
+import { registerCoreBlocks } from '@wordpress/block-library';
+import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
+import '@wordpress/format-library';
+import {
+ createBlock,
+ unregisterBlockType,
+ getBlockTypes,
+} from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { waitForStoreResolvers } from './wait-for-store-resolvers';
+
+// Polyfill for String.prototype.replaceAll until CI is runnig Node 15 or higher.
+if ( ! String.prototype.replaceAll ) {
+ String.prototype.replaceAll = function ( str, newStr ) {
+ // If a regex pattern
+ if (
+ Object.prototype.toString.call( str ).toLowerCase() ===
+ '[object regexp]'
+ ) {
+ return this.replace( str, newStr );
+ }
+
+ // If a string
+ return this.replace( new RegExp( str, 'g' ), newStr );
+ };
+}
+
+/**
+ * Selects the block to be tested by the aria-label on the block wrapper, eg. "Block: Cover".
+ *
+ * @param {string} name
+ */
+export async function selectBlock( name ) {
+ await userEvent.click( screen.getByLabelText( name ) );
+}
+
+export function Editor( { testBlocks, settings = {} } ) {
+ const [ currentBlocks, updateBlocks ] = useState( testBlocks );
+
+ useEffect( () => {
+ return () => {
+ getBlockTypes().forEach( ( { name } ) =>
+ unregisterBlockType( name )
+ );
+ };
+ }, [] );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Registers the core block, creates the test block instances, and then instantiates the Editor.
+ *
+ * @param {Object | Array} testBlocks Block or array of block settings for blocks to be tested.
+ * @param {boolean} useCoreBlocks Defaults to true. If false, core blocks will not be registered.
+ * @param {Object} settings Any additional editor settings to be passed to the editor.
+ */
+export async function initializeEditor(
+ testBlocks,
+ useCoreBlocks = true,
+ settings
+) {
+ if ( useCoreBlocks ) {
+ registerCoreBlocks();
+ }
+ const blocks = Array.isArray( testBlocks ) ? testBlocks : [ testBlocks ];
+ const newBlocks = blocks.map( ( testBlock ) =>
+ createBlock(
+ testBlock.name,
+ testBlock.attributes,
+ testBlock.innerBlocks
+ )
+ );
+ return waitForStoreResolvers( () => {
+ return render(
+
+ );
+ } );
+}
diff --git a/test/integration/helpers/wait-for-store-resolvers.js b/test/integration/helpers/wait-for-store-resolvers.js
new file mode 100644
index 0000000000000..fbb35c4e73461
--- /dev/null
+++ b/test/integration/helpers/wait-for-store-resolvers.js
@@ -0,0 +1,40 @@
+/**
+ * External dependencies
+ */
+import { act } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { withFakeTimers } from './with-fake-timers';
+
+/**
+ * Executes a function that triggers store resolvers and waits for them to be finished.
+ *
+ * Asynchronous store resolvers leverage `setTimeout` to run at the end of
+ * the current JavaScript block execution. In order to prevent "act" warnings
+ * triggered by updates to the React tree, we manually tick fake timers and
+ * await the resolution of the current block execution before proceeding.
+ *
+ * @param {Function} fn Function that to trigger.
+ *
+ * @return {*} The result of the function call.
+ */
+export async function waitForStoreResolvers( fn ) {
+ return withFakeTimers( async () => {
+ const result = fn();
+
+ // Advance all timers allowing store resolvers to resolve.
+ act( () => jest.runAllTimers() );
+
+ // The store resolvers perform several API fetches during editor
+ // initialization. The most straightforward approach to ensure all of them
+ // resolve before we consider the editor initialized is to flush micro tasks,
+ // similar to the approach found in `@testing-library/react`.
+ // https://github.com/callstack/react-native-testing-library/blob/a010ffdbca906615279ecc3abee423525e528101/src/flushMicroTasks.js#L15-L23.
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act( async () => {} );
+
+ return result;
+ } );
+}
diff --git a/test/integration/helpers/with-fake-timers.js b/test/integration/helpers/with-fake-timers.js
new file mode 100644
index 0000000000000..91e3ead0a2bf0
--- /dev/null
+++ b/test/integration/helpers/with-fake-timers.js
@@ -0,0 +1,30 @@
+/**
+ * Set up fake timers for executing a function and restores them afterwards.
+ *
+ * @param {Function} fn Function to trigger.
+ *
+ * @return {*} The result of the function call.
+ */
+export async function withFakeTimers( fn ) {
+ const usingFakeTimers = jest.isMockFunction( setTimeout );
+
+ // Portions of the React Native Animation API rely upon these APIs. However,
+ // Jest's 'legacy' fake timers mutate these globals, which breaks the Animated
+ // API. We preserve the original implementations to restore them later.
+ const requestAnimationFrameCopy = global.requestAnimationFrame;
+ const cancelAnimationFrameCopy = global.cancelAnimationFrame;
+
+ if ( ! usingFakeTimers ) {
+ jest.useFakeTimers( { legacyFakeTimers: true } );
+ }
+
+ const result = await fn();
+
+ if ( ! usingFakeTimers ) {
+ jest.useRealTimers();
+
+ global.requestAnimationFrame = requestAnimationFrameCopy;
+ global.cancelAnimationFrame = cancelAnimationFrameCopy;
+ }
+ return result;
+}