diff --git a/lib/compat/wordpress-6.2/edit-form-blocks.php b/lib/compat/wordpress-6.2/edit-form-blocks.php new file mode 100644 index 0000000000000..5031550abc154 --- /dev/null +++ b/lib/compat/wordpress-6.2/edit-form-blocks.php @@ -0,0 +1,24 @@ + { + if ( debounced !== input ) { + setDebounced( input ); + } + }, [ debounced, input ] ); + return [ input, setInput, debounced ]; +} + +export function useMediaResults( options = {} ) { + const [ results, setResults ] = useState(); + const settings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + useEffect( () => { + ( async () => { + setResults(); + const _media = await settings?.__unstableFetchMedia( options ); + if ( _media ) setResults( _media ); + } )(); + }, Object.values( options ) ); + return results; +} + +const MEDIA_CATEGORIES = [ + { label: __( 'Images' ), name: 'images', mediaType: 'image' }, + { label: __( 'Videos' ), name: 'videos', mediaType: 'video' }, + { label: __( 'Audio' ), name: 'audio', mediaType: 'audio' }, +]; +export function useMediaCategories( rootClientId ) { + const [ categories, setCategories ] = useState( [] ); + const { canInsertImage, canInsertVideo, canInsertAudio, fetchMedia } = + useSelect( + ( select ) => { + const { canInsertBlockType, getSettings } = + select( blockEditorStore ); + return { + fetchMedia: getSettings().__unstableFetchMedia, + canInsertImage: canInsertBlockType( + 'core/image', + rootClientId + ), + canInsertVideo: canInsertBlockType( + 'core/video', + rootClientId + ), + canInsertAudio: canInsertBlockType( + 'core/audio', + rootClientId + ), + }; + }, + [ rootClientId ] + ); + useEffect( () => { + ( async () => { + // If `__unstableFetchMedia` is not defined in block + // editor settings, do not set any media categories. + if ( ! fetchMedia ) return; + const query = { + context: 'view', + per_page: 1, + _fields: [ 'id' ], + }; + const [ image, video, audio ] = await Promise.all( [ + fetchMedia( { ...query, media_type: 'image' } ), + fetchMedia( { ...query, media_type: 'video' } ), + fetchMedia( { ...query, media_type: 'audio' } ), + ] ); + const showImage = canInsertImage && !! image.length; + const showVideo = canInsertVideo && !! video.length; + const showAudio = canInsertAudio && !! audio.length; + setCategories( + MEDIA_CATEGORIES.filter( + ( { mediaType } ) => + ( mediaType === 'image' && showImage ) || + ( mediaType === 'video' && showVideo ) || + ( mediaType === 'audio' && showAudio ) + ) + ); + } )(); + }, [ canInsertImage, canInsertVideo, canInsertAudio, fetchMedia ] ); + return categories; +} diff --git a/packages/block-editor/src/components/inserter/media-tab/index.js b/packages/block-editor/src/components/inserter/media-tab/index.js new file mode 100644 index 0000000000000..6e4c518ff63b5 --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/index.js @@ -0,0 +1,3 @@ +export { default as MediaTab } from './media-tab'; +export { MediaCategoryDialog } from './media-panel'; +export { useMediaCategories } from './hooks'; diff --git a/packages/block-editor/src/components/inserter/media-tab/media-list.js b/packages/block-editor/src/components/inserter/media-tab/media-list.js new file mode 100644 index 0000000000000..da77585583d8c --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/media-list.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { + __unstableComposite as Composite, + __unstableUseCompositeState as useCompositeState, + __unstableCompositeItem as CompositeItem, + Tooltip, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useMemo, useCallback } from '@wordpress/element'; +import { cloneBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InserterDraggableBlocks from '../../inserter-draggable-blocks'; +import { getBlockAndPreviewFromMedia } from './utils'; + +function MediaPreview( { media, onClick, composite, mediaType } ) { + const [ block, preview ] = useMemo( + () => getBlockAndPreviewFromMedia( media, mediaType ), + [ media, mediaType ] + ); + const title = media.title?.rendered || media.title; + const baseCssClass = 'block-editor-inserter__media-list'; + return ( + + { ( { draggable, onDragStart, onDragEnd } ) => ( +
+ + { + onClick( block ); + } } + aria-label={ title } + > +
+ { preview } +
+
+
+
+ ) } +
+ ); +} + +function MediaList( { + mediaList, + mediaType, + onClick, + label = __( 'Media List' ), +} ) { + const composite = useCompositeState(); + const onPreviewClick = useCallback( + ( block ) => { + onClick( cloneBlock( block ) ); + }, + [ onClick ] + ); + return ( + + { mediaList.map( ( media ) => ( + + ) ) } + + ); +} + +export default MediaList; diff --git a/packages/block-editor/src/components/inserter/media-tab/media-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js new file mode 100644 index 0000000000000..a73c51bc8e8e0 --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; +import { Spinner, SearchControl } from '@wordpress/components'; +import { focus } from '@wordpress/dom'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import MediaList from './media-list'; +import { useMediaResults, useDebouncedInput } from './hooks'; +import InserterNoResults from '../no-results'; + +const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; + +export function MediaCategoryDialog( { rootClientId, onInsert, category } ) { + const container = useRef(); + useEffect( () => { + const timeout = setTimeout( () => { + const [ firstTabbable ] = focus.tabbable.find( container.current ); + firstTabbable?.focus(); + } ); + return () => clearTimeout( timeout ); + }, [ category ] ); + return ( +
+ +
+ ); +} + +export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { + const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); + const mediaList = useMediaResults( { + per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, + media_type: category.mediaType, + search: debouncedSearch, + orderBy: !! debouncedSearch ? 'relevance' : 'date', + } ); + const baseCssClass = 'block-editor-inserter__media-panel'; + return ( +
+ + { ! mediaList && ( +
+ +
+ ) } + { Array.isArray( mediaList ) && ! mediaList.length && ( + + ) } + { !! mediaList?.length && ( + + ) } +
+ ); +} diff --git a/packages/block-editor/src/components/inserter/media-tab/media-tab.js b/packages/block-editor/src/components/inserter/media-tab/media-tab.js new file mode 100644 index 0000000000000..639a63df271d0 --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/media-tab.js @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __, isRTL } from '@wordpress/i18n'; +import { useViewportMatch } from '@wordpress/compose'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalItem as Item, + __experimentalHStack as HStack, + __experimentalNavigatorProvider as NavigatorProvider, + __experimentalNavigatorScreen as NavigatorScreen, + __experimentalNavigatorButton as NavigatorButton, + __experimentalNavigatorBackButton as NavigatorBackButton, + FlexBlock, + Button, +} from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; +import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { MediaCategoryPanel } from './media-panel'; +import MediaUploadCheck from '../../media-upload/check'; +import MediaUpload from '../../media-upload'; +import { useMediaCategories } from './hooks'; +import { getBlockAndPreviewFromMedia } from './utils'; + +const ALLOWED_MEDIA_TYPES = [ 'image', 'video', 'audio' ]; + +function MediaTab( { + rootClientId, + selectedCategory, + onSelectCategory, + onInsert, +} ) { + const mediaCategories = useMediaCategories( rootClientId ); + const isMobile = useViewportMatch( 'medium', '<' ); + const baseCssClass = 'block-editor-inserter__media-tabs'; + const onSelectMedia = useCallback( + ( media ) => { + if ( ! media?.url ) { + return; + } + const [ block ] = getBlockAndPreviewFromMedia( media, media.type ); + onInsert( block ); + }, + [ onInsert ] + ); + return ( + <> + { ! isMobile && ( +
+ +
+ ) } + { isMobile && ( + + ) } + + ); +} + +function MediaTabNavigation( { onInsert, rootClientId, mediaCategories } ) { + return ( + + + + { mediaCategories.map( ( category ) => ( + + + { category.label } + + + + ) ) } + + + { mediaCategories.map( ( category ) => ( + + + { __( 'Back' ) } + + + + ) ) } + + ); +} + +export default MediaTab; diff --git a/packages/block-editor/src/components/inserter/media-tab/utils.js b/packages/block-editor/src/components/inserter/media-tab/utils.js new file mode 100644 index 0000000000000..f6a18514d728c --- /dev/null +++ b/packages/block-editor/src/components/inserter/media-tab/utils.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +const mediaTypeTag = { image: 'img', video: 'video', audio: 'audio' }; + +export function getBlockAndPreviewFromMedia( media, mediaType ) { + // Add the common attributes between the different media types. + const attributes = { + id: media.id, + }; + // Some props are named differently between the Media REST API and Media Library API. + // For example `source_url` is used in the former and `url` is used in the latter. + const mediaSrc = media.source_url || media.url; + const alt = media.alt_text || media.alt || undefined; + const caption = media.caption?.raw || media.caption; + if ( caption && typeof caption === 'string' ) { + attributes.caption = caption; + } + if ( mediaType === 'image' ) { + attributes.url = mediaSrc; + attributes.alt = alt; + } else if ( [ 'video', 'audio' ].includes( mediaType ) ) { + attributes.src = mediaSrc; + } + const PreviewTag = mediaTypeTag[ mediaType ]; + const preview = ( + + ); + return [ createBlock( `core/${ mediaType }`, attributes ), preview ]; +} diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index c6d019c8ff9a7..37a09dd448870 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -28,6 +28,7 @@ import BlockPatternsTabs, { BlockPatternsCategoryDialog, } from './block-patterns-tab'; import ReusableBlocksTab from './reusable-blocks-tab'; +import { MediaTab, MediaCategoryDialog, useMediaCategories } from './media-tab'; import InserterSearchResults from './search-results'; import useInsertionPoint from './hooks/use-insertion-point'; import InserterTabs from './tabs'; @@ -54,6 +55,8 @@ function InserterMenu( const [ hoveredItem, setHoveredItem ] = useState( null ); const [ selectedPatternCategory, setSelectedPatternCategory ] = useState( null ); + const [ selectedMediaCategory, setSelectedMediaCategory ] = + useState( null ); const [ selectedTab, setSelectedTab ] = useState( null ); const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] = @@ -68,7 +71,6 @@ function InserterMenu( ( select ) => { const { __experimentalGetAllowedPatterns, getSettings } = select( blockEditorStore ); - return { showPatterns: !! __experimentalGetAllowedPatterns( destinationRootClientId @@ -80,6 +82,9 @@ function InserterMenu( [ destinationRootClientId ] ); + const mediaCategories = useMediaCategories( destinationRootClientId ); + const showMedia = !! mediaCategories.length; + const onInsert = useCallback( ( blocks, meta, shouldForceFocusBlock ) => { onInsertBlocks( blocks, meta, shouldForceFocusBlock ); @@ -170,16 +175,36 @@ function InserterMenu( [ destinationRootClientId, onInsert, onHover ] ); + const mediaTab = useMemo( + () => ( + + ), + [ + destinationRootClientId, + onInsert, + selectedMediaCategory, + setSelectedMediaCategory, + ] + ); + const getCurrentTab = useCallback( ( tab ) => { if ( tab.name === 'blocks' ) { return blocksTab; } else if ( tab.name === 'patterns' ) { return patternsTab; + } else if ( tab.name === 'reusable' ) { + return reusableBlocksTab; + } else if ( tab.name === 'media' ) { + return mediaTab; } - return reusableBlocksTab; }, - [ blocksTab, patternsTab, reusableBlocksTab ] + [ blocksTab, patternsTab, reusableBlocksTab, mediaTab ] ); const searchRef = useRef(); @@ -191,8 +216,10 @@ function InserterMenu( const showPatternPanel = selectedTab === 'patterns' && ! filterValue && selectedPatternCategory; - const showAsTabs = ! filterValue && ( showPatterns || hasReusableBlocks ); - + const showAsTabs = + ! filterValue && ( showPatterns || hasReusableBlocks || showMedia ); + const showMediaPanel = + selectedTab === 'media' && ! filterValue && selectedMediaCategory; return (
@@ -244,6 +272,13 @@ function InserterMenu(
) }
+ { showMediaPanel && ( + + ) } { showInserterHelpPanel && hoveredItem && ( ) } diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index ad5a2d6642b4a..dc1036a5b5c26 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -480,3 +480,151 @@ $block-inserter-tabs-height: 44px; .block-editor-inserter__patterns-category-panel-title { font-size: calc(1.25 * 13px); } + +.block-editor-inserter__media-tabs-container { + height: 100%; + + nav { + height: 100%; + } + + .block-editor-inserter__media-library-button { + padding: $grid-unit-20; + justify-content: center; + margin-top: $grid-unit-20; + width: 100%; + } +} + +.block-editor-inserter__media-tabs { + display: flex; + flex-direction: column; + padding: $grid-unit-20; + overflow-y: auto; + height: 100%; + + // Push the listitem wrapping the "open media library" button to the bottom of the panel. + div[role="listitem"]:last-child { + margin-top: auto; + } + + &__media-category { + &.is-selected { + color: var(--wp-admin-theme-color); + position: relative; + + .components-flex-item { + filter: brightness(0.95); + } + + svg { + fill: var(--wp-admin-theme-color); + } + + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-radius: $radius-block-ui; + opacity: 0.04; + background: var(--wp-admin-theme-color); + } + } + } +} + +.block-editor-inserter__media-dialog { + background: $gray-100; + border-left: $border-width solid $gray-200; + border-right: $border-width solid $gray-200; + position: absolute; + padding: $grid-unit-20 $grid-unit-30; + top: 0; + left: 0; + height: 100%; + width: 100%; + overflow-y: auto; + scrollbar-gutter: stable both-edges; + + @include break-medium { + left: 100%; + display: block; + width: 300px; + } + + .block-editor-block-preview__container { + box-shadow: 0 15px 25px rgb(0 0 0 / 7%); + &:hover { + box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%); + } + } + + .block-editor-inserter__media-panel { + height: 100%; + + &-title { + font-size: calc(1.25 * 13px); + } + + &-spinner { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + &-search { + &.components-search-control { + input[type="search"].components-search-control__input { + background: $white; + } + } + } + } +} + +.block-editor-inserter__media-list { + margin-top: $grid-unit-20; + &__list-item { + cursor: pointer; + margin-bottom: $grid-unit-30; + + &.is-placeholder { + min-height: 100px; + } + + &[draggable="true"] .block-editor-block-preview__container { + cursor: grab; + } + } + + &__item { + height: 100%; + + &-preview { + display: flex; + align-items: center; + overflow: hidden; + border-radius: 4px; + + > * { + margin: 0 auto; + max-width: 100%; + } + } + + &:hover &-preview { + box-shadow: 0 0 0 2px var(--wp-admin-theme-color); + } + + &:focus &-preview { + box-shadow: inset 0 0 0 1px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + } +} diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 24bfade8b2f4b..8a80e96cb78cc 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -22,11 +22,17 @@ const reusableBlocksTab = { title: __( 'Reusable' ), icon: reusableBlockIcon, }; +const mediaTab = { + name: 'media', + /* translators: Media tab title in the block inserter. */ + title: __( 'Media' ), +}; function InserterTabs( { children, showPatterns = false, showReusableBlocks = false, + showMedia = false, onSelect, prioritizePatterns, } ) { @@ -39,10 +45,12 @@ function InserterTabs( { if ( ! prioritizePatterns && showPatterns ) { tempTabs.push( patternsTab ); } + if ( showMedia ) { + tempTabs.push( mediaTab ); + } if ( showReusableBlocks ) { tempTabs.push( reusableBlocksTab ); } - return tempTabs; }, [ prioritizePatterns, @@ -50,6 +58,7 @@ function InserterTabs( { showPatterns, patternsTab, showReusableBlocks, + showMedia, reusableBlocksTab, ] ); diff --git a/packages/compose/src/hooks/use-focus-outside/index.ts b/packages/compose/src/hooks/use-focus-outside/index.ts index cbf1d45d8c9e4..0d62318d38eca 100644 --- a/packages/compose/src/hooks/use-focus-outside/index.ts +++ b/packages/compose/src/hooks/use-focus-outside/index.ts @@ -151,6 +151,23 @@ export default function useFocusOutside( return; } + // The usage of this attribute should be avoided. The only use case + // would be when we load modals that are not React components and + // therefore don't exist in the React tree. An example is opening + // the Media Library modal from another dialog. + // This attribute should contain a selector of the related target + // we want to ignore, because we still need to trigger the blur event + // on all other cases. + const ignoreForRelatedTarget = event.target.getAttribute( + 'data-unstable-ignore-focus-outside-for-relatedtarget' + ); + if ( + ignoreForRelatedTarget && + event.relatedTarget?.closest( ignoreForRelatedTarget ) + ) { + return; + } + blurCheckTimeoutId.current = setTimeout( () => { // If document is not focused then focus should remain // inside the wrapped component and therefore we cancel diff --git a/packages/core-data/src/fetch/fetch-media.js b/packages/core-data/src/fetch/fetch-media.js new file mode 100644 index 0000000000000..c7a3a429a8627 --- /dev/null +++ b/packages/core-data/src/fetch/fetch-media.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { resolveSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_NAME as coreStore } from '../name'; + +export default async function fetchMedia( settings = {} ) { + return resolveSelect( coreStore ).getMediaItems( settings ); +} diff --git a/packages/core-data/src/fetch/index.js b/packages/core-data/src/fetch/index.js index 8d4d28e3b0db8..2f765825f3e70 100644 --- a/packages/core-data/src/fetch/index.js +++ b/packages/core-data/src/fetch/index.js @@ -1,2 +1,3 @@ export { default as __experimentalFetchLinkSuggestions } from './__experimental-fetch-link-suggestions'; export { default as __experimentalFetchUrlData } from './__experimental-fetch-url-data'; +export { default as __experimentalFetchMedia } from './fetch-media'; diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 0f42214b45442..f990ee7f15f69 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -8,7 +8,11 @@ import classnames from 'classnames'; */ import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useMemo, useRef, Fragment } from '@wordpress/element'; -import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data'; +import { + useEntityBlockEditor, + __experimentalFetchMedia as fetchMedia, + store as coreStore, +} from '@wordpress/core-data'; import { BlockList, BlockEditorProvider, @@ -131,6 +135,7 @@ export default function BlockEditor( { setIsInserterOpen } ) { return { ...restStoredSettings, + __unstableFetchMedia: fetchMedia, __experimentalBlockPatterns: blockPatterns, __experimentalBlockPatternCategories: blockPatternCategories, }; diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 0b69dcc66eb81..69b4f9b42cfc6 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -7,6 +7,7 @@ import { store as coreStore, __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, + __experimentalFetchMedia as fetchMedia, } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; @@ -182,6 +183,9 @@ function useBlockEditorSettings( settings, hasTemplate ) { __experimentalBlockPatternCategories: blockPatternCategories, __experimentalFetchLinkSuggestions: ( search, searchOptions ) => fetchLinkSuggestions( search, searchOptions, settings ), + // TODO: We should find a proper way to consolidate similar cases + // like reusable blocks, fetch entities, etc. + __unstableFetchMedia: fetchMedia, __experimentalFetchRichUrlData: fetchUrlData, __experimentalCanUserUseUnfilteredHTML: canUseUnfilteredHTML, __experimentalUndo: undo, diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index cc75245d48bac..f45a911650823 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +const path = require( 'path' ); + /** * WordPress dependencies */ @@ -286,6 +291,50 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { } ); } ); +test.describe( 'insert media from inserter', () => { + let uploadedMedia; + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + uploadedMedia = await requestUtils.uploadMedia( + path.resolve( + process.cwd(), + 'test/e2e/assets/10x10_e2e_test_image_z9T8jK.png' + ) + ); + } ); + test.afterAll( async ( { requestUtils } ) => { + Promise.all( [ + requestUtils.deleteAllMedia(), + requestUtils.deleteAllPosts(), + ] ); + } ); + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + test( 'insert media from the global inserter', async ( { + page, + editor, + } ) => { + await page.click( + 'role=region[name="Editor top bar"i] >> role=button[name="Toggle block inserter"i]' + ); + await page.click( + 'role=region[name="Block Library"i] >> role=tab[name="Media"i]' + ); + await page.click( + '[aria-label="Media categories"i] >> role=button[name="Images"i]' + ); + await page.click( + `role=listbox[name="Media List"i] >> role=option[name="${ uploadedMedia.title.raw }"]` + ); + await expect.poll( editor.getEditedPostContent ).toBe( + ` +
${ uploadedMedia.alt_text }
+` + ); + } ); +} ); + class InsertingBlocksUtils { constructor( { page, editor } ) { this.page = page;