diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 8d1ae7c47b1dfa..96ed7f3dcb1ea4 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -111,7 +111,7 @@ _Parameters_ ### closeGlobalBlockInserter -Undocumented declaration. +Closes the global inserter. ### closeListView @@ -455,41 +455,37 @@ _Returns_ ### insertBlock -Opens the inserter, searches for the given term, then selects the first -result that appears. It then waits briefly for the block list to update. +Inserts a block matching a given search term via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the block to insert. ### insertBlockDirectoryBlock -Opens the inserter, searches for the given block, then selects the -first result that appears from the block directory. It then waits briefly for the block list to -update. +Inserts a Block Directory block matching a given search term via the global +inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the Block Directory block to insert. ### insertPattern -Opens the inserter, searches for the given pattern, then selects the first -result that appears. It then waits briefly for the block list to update. +Inserts a pattern matching a given search term via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the pattern to insert. ### insertReusableBlock -Opens the inserter, searches for the given reusable block, then selects the -first result that appears. It then waits briefly for the block list to -update. +Inserts a reusable block matching a given search term via the global +inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the reusable block to insert. ### installPlugin @@ -577,7 +573,7 @@ Clicks on the button in the header which opens Document Settings sidebar when it ### openGlobalBlockInserter -Opens the global block inserter. +Opens the global inserter. ### openGlobalStylesPanel @@ -667,27 +663,51 @@ _Returns_ ### searchForBlock -Search for block in the global inserter +Searches for a block via the global inserter. + +_Parameters_ + +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of block to be inserted or null if nothing was found. + +### searchForBlockDirectoryBlock + +Searches for a Block Directory block via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the Block Directory block to be inserted or null if nothing was found. ### searchForPattern -Search for pattern in the global inserter +Searches for a pattern via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the pattern to be inserted or null if nothing was found. ### searchForReusableBlock -Search for reusable block in the global inserter. +Searches for a reusable block via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the reusable block to be inserted or null if nothing was found. ### selectBlockByClientId diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index fda35292174b00..6b217d52c44074 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -42,16 +42,17 @@ export { hasBlockSwitcher } from './has-block-switcher'; export { getPageError } from './get-page-error'; export { getOption } from './get-option'; export { - insertBlock, - insertPattern, - insertReusableBlock, + openGlobalBlockInserter, + closeGlobalBlockInserter, + toggleGlobalBlockInserter, searchForBlock, searchForPattern, searchForReusableBlock, + searchForBlockDirectoryBlock, + insertBlock, + insertPattern, + insertReusableBlock, insertBlockDirectoryBlock, - openGlobalBlockInserter, - closeGlobalBlockInserter, - toggleGlobalBlockInserter, } from './inserter'; export { installPlugin } from './install-plugin'; export { installTheme } from './install-theme'; diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index e69282c9dd2bf5..84521728160bee 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { ElementHandle } from 'puppeteer-core'; + /** * Internal dependencies */ @@ -10,38 +15,37 @@ const INSERTER_SEARCH_SELECTOR = '.block-editor-inserter__search input,.block-editor-inserter__search-input,input.block-editor-inserter__search'; /** - * Opens the global block inserter. + * Opens the global inserter. */ export async function openGlobalBlockInserter() { - if ( await isGlobalInserterOpen() ) { - // If global inserter is already opened, reset to an initial state where - // the default (first) tab is selected. - const tab = await page.$( - '.block-editor-inserter__tabs .components-tab-panel__tabs-item:nth-of-type(1):not(.is-active)' - ); - - if ( tab ) { - await tab.click(); - } - } else { + if ( ! ( await isGlobalInserterOpen() ) ) { await toggleGlobalBlockInserter(); - // Waiting here is necessary because sometimes the inserter takes more time to - // render than Puppeteer takes to complete the 'click' action. + // Waiting here is necessary because sometimes the inserter takes more + // time to render than Puppeteer takes to complete the 'click' action. await page.waitForSelector( '.block-editor-inserter__menu' ); } } +/** + * Closes the global inserter. + */ export async function closeGlobalBlockInserter() { if ( await isGlobalInserterOpen() ) { await toggleGlobalBlockInserter(); } } +/** + * Checks if the global inserter is open. + * + * @return {Promise} Whether the inserter is open or not. + */ async function isGlobalInserterOpen() { return await page.evaluate( () => { // "Add block" selector is required to make sure performance comparison - // doesn't fail on older branches where we still had "Add block" as label. + // doesn't fail on older branches where we still had "Add block" as + // label. return !! document.querySelector( '.edit-post-header [aria-label="Add block"].is-pressed,' + '.edit-site-header-edit-mode [aria-label="Add block"].is-pressed,' + @@ -70,11 +74,56 @@ export async function toggleGlobalBlockInserter() { ); } +/** + * Selects the global inserter tab/category, unless it's already selected. + * + * @param {string} label The label of the tab to select. + */ +export async function selectGlobalInserterTab( label ) { + const tabs = await page.$( '.block-editor-inserter__tabs' ); + if ( ! tabs ) { + return; // Do nothing if tabs are unavailable (e.g. for inner blocks). + } + + const activeTab = await page.waitForSelector( + '.block-editor-inserter__tabs button.is-active' + ); + + const activeTabLabel = await page.evaluate( + ( el ) => el.innerText, + activeTab + ); + + if ( activeTabLabel === label ) { + return; // Do nothing if the target tab is already active. + } + + let labelSelector; + + switch ( label ) { + case 'Blocks': + case 'Patterns': + case 'Media': + labelSelector = `. = "${ label }"`; + break; + case 'Reusable': + // Reusable tab label is an icon, hence the different selector. + labelSelector = `@aria-label = "${ label }"`; + break; + } + + const targetTab = await page.waitForXPath( + `//div[contains(@class, "block-editor-inserter__tabs")]//button[${ labelSelector }]` + ); + + await targetTab.click(); +} + /** * Moves focus to the selected block. */ async function focusSelectedBlock() { - // Ideally there shouuld be a UI way to do this. (Focus the selected block) + // Ideally, there should be a UI way to focus the selected block. await page.evaluate( () => { wp.data .dispatch( 'core/block-editor' ) @@ -88,7 +137,8 @@ async function focusSelectedBlock() { } /** - * Retrieves the document container by css class and checks to make sure the document's active element is within it + * Retrieves the document container by css class and checks to make sure the + * document's active element is within it. */ async function waitForInserterCloseAndContentFocus() { await canvas().waitForFunction( @@ -100,157 +150,213 @@ async function waitForInserterCloseAndContentFocus() { } /** - * Wait for the inserter search to yield results because that input is debounced. + * Searches for an entity matching given category and term via the global + * inserter. If nothing is found, null will be returned. + * + * Available categories: Blocks, Patterns, Reusable and Block Directory. + * + * @param {string} category The category to search within. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the element to be + * inserted or null if nothing was found. + */ +export async function searchGlobalInserter( category, searchTerm ) { + await openGlobalBlockInserter(); + await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); + await page.focus( INSERTER_SEARCH_SELECTOR ); + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.type( searchTerm ); + + // Wait for the default block list to disappear to prevent its items from + // being considered as search results. This is needed since we're debouncing + // search request. + await page.waitForSelector( '.block-editor-inserter__block-list', { + hidden: true, + } ); + + let waitForInsertElement; + let waitForNoResults; + + switch ( category ) { + case 'Blocks': + case 'Patterns': + case 'Reusable': { + waitForInsertElement = async () => { + return await page.waitForXPath( + `//*[@role='option' and contains(., '${ searchTerm }')]` + ); + }; + waitForNoResults = async () => { + await page.waitForSelector( + '.block-editor-inserter__no-results' + ); + return null; + }; + break; + } + case 'Block Directory': { + waitForInsertElement = async () => { + // Return the first item from the Block Directory search results. + return await page.waitForSelector( + '.block-directory-downloadable-blocks-list button:first-child' + ); + }; + waitForNoResults = async () => { + // Use a soft timeout if Block Directory doesn't return anything + // within 5 seconds, as there's no "empty results" element being + // rendered when nothing is found. + return await new Promise( ( resolve ) => + setTimeout( () => resolve( null ), 5000 ) + ); + }; + } + } + + return await Promise.race( [ waitForInsertElement(), waitForNoResults() ] ); +} + +/** + * Inserts an entity matching given category and term via the global inserter. + * If the entity is not instantly available in the open inserter, a search will + * be performed. If the search returns no results, an error will be thrown. + * + * Available categories: Blocks, Patterns, Reusable and Block Directory. + * + * @param {string} category The category to insert from. + * @param {string} searchTerm The term by which to find the entity to insert. */ -async function waitForInserterSearch() { - try { +export async function insertFromGlobalInserter( category, searchTerm ) { + await openGlobalBlockInserter(); + await selectGlobalInserterTab( category ); + + let insertButton; + + if ( [ 'Blocks', 'Reusable' ].includes( category ) ) { + // If it's a block, see it it's insertable without searching... + try { + insertButton = ( + await page.$x( + `//*[@role='option' and contains(., '${ searchTerm }')]` + ) + )[ 0 ]; + } catch ( error ) { + // noop + } + } + + // ...and if not, perform a global search. + if ( ! insertButton ) { + insertButton = await searchGlobalInserter( category, searchTerm ); + } + + // Throw an error if nothing was found. + if ( ! insertButton ) { + throw new Error( + `Couldn't find "${ searchTerm }" in the ${ category } category.` + ); + } + + // Insert found entity. + await insertButton.click(); + + // Extra wait for the reusable block to be ready. + if ( category === 'Reusable' ) { await page.waitForSelector( - '.block-editor-inserter__no-tab-container', - { timeout: 2000 } + '.block-library-block__reusable-block-container' ); - } catch ( e ) { - // This selector doesn't exist in older versions, so let's just continue. } + + // Extra wait for the Block Directory block to be ready. + if ( category === 'Block Directory' ) { + await page.waitForSelector( + '.block-directory-downloadable-blocks-list button:first-child:not(.is-busy)' + ); + } + + await focusSelectedBlock(); + await waitForInserterCloseAndContentFocus(); } /** - * Search for block in the global inserter + * Searches for a block via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of block to be + * inserted or null if nothing was found. */ export async function searchForBlock( searchTerm ) { - await openGlobalBlockInserter(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); + return await searchGlobalInserter( 'Blocks', searchTerm ); } /** - * Search for pattern in the global inserter + * Searches for a pattern via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the pattern to be + * inserted or null if nothing was found. */ export async function searchForPattern( searchTerm ) { - await openGlobalBlockInserter(); - // Select the patterns tab. - const tab = await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[.="Patterns"]' - ); - await tab.click(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); + return await searchGlobalInserter( 'Patterns', searchTerm ); } /** - * Search for reusable block in the global inserter. + * Searches for a reusable block via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the reusable block to be + * inserted or null if nothing was found. */ export async function searchForReusableBlock( searchTerm ) { - await openGlobalBlockInserter(); - - // The reusable blocks tab won't appear until the reusable blocks have been - // fetched. They aren't fetched until an inserter is used or the post - // already contains reusable blocks, so wait for the tab to appear. - await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[@aria-label="Reusable"]' - ); + return await searchGlobalInserter( 'Reusable', searchTerm ); +} - // Select the reusable blocks tab. - const tab = await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[@aria-label="Reusable"]' - ); - await tab.click(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); +/** + * Searches for a Block Directory block via the global inserter. + * + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the Block Directory block + * to be inserted or null if nothing was found. + */ +export async function searchForBlockDirectoryBlock( searchTerm ) { + return await searchGlobalInserter( 'Block Directory', searchTerm ); } /** - * Opens the inserter, searches for the given term, then selects the first - * result that appears. It then waits briefly for the block list to update. + * Inserts a block matching a given search term via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the block to insert. */ export async function insertBlock( searchTerm ) { - await searchForBlock( searchTerm ); - const insertButton = await page.waitForXPath( - `//button//span[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + await insertFromGlobalInserter( 'Blocks', searchTerm ); } /** - * Opens the inserter, searches for the given pattern, then selects the first - * result that appears. It then waits briefly for the block list to update. + * Inserts a pattern matching a given search term via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the pattern to insert. */ export async function insertPattern( searchTerm ) { - await searchForPattern( searchTerm ); - const insertButton = await page.waitForXPath( - `//div[@role = 'option']//div[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + await insertFromGlobalInserter( 'Patterns', searchTerm ); } /** - * Opens the inserter, searches for the given reusable block, then selects the - * first result that appears. It then waits briefly for the block list to - * update. + * Inserts a reusable block matching a given search term via the global + * inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the reusable block to + * insert. */ export async function insertReusableBlock( searchTerm ) { - await searchForReusableBlock( searchTerm ); - const insertButton = await page.waitForXPath( - `//button//span[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); - // We should wait until the block is loaded. - await page.waitForXPath( - '//*[contains(@class,"block-library-block__reusable-block-container")]' - ); + await insertFromGlobalInserter( 'Reusable', searchTerm ); } /** - * Opens the inserter, searches for the given block, then selects the - * first result that appears from the block directory. It then waits briefly for the block list to - * update. + * Inserts a Block Directory block matching a given search term via the global + * inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the Block Directory + * block to insert. */ export async function insertBlockDirectoryBlock( searchTerm ) { - await searchForBlock( searchTerm ); - - // Grab the first block in the list. - const insertButton = await page.waitForSelector( - '.block-directory-downloadable-blocks-list button:first-child' - ); - await insertButton.click(); - await page.waitForFunction( - () => - ! document.body.querySelector( - '.block-directory-downloadable-blocks-list button:first-child.is-busy' - ) - ); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + return await insertFromGlobalInserter( 'Block Directory', searchTerm ); } diff --git a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js index d1537263968192..2e969d17915924 100644 --- a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js +++ b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js @@ -166,7 +166,7 @@ const matchUrl = ( reqUrl, urls ) => { }; describe( 'adding blocks from block directory', () => { - beforeEach( async () => { + beforeAll( async () => { await createNewPost(); } );