diff --git a/packages/customize-widgets/src/controls/sidebar-section.js b/packages/customize-widgets/src/controls/sidebar-section.js index a30f4c242b035..33bb366b5233c 100644 --- a/packages/customize-widgets/src/controls/sidebar-section.js +++ b/packages/customize-widgets/src/controls/sidebar-section.js @@ -16,6 +16,14 @@ export default function getSidebarSection() { wp: { customize }, } = window; + const reduceMotionMediaQuery = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ); + let isReducedMotion = reduceMotionMediaQuery.matches; + reduceMotionMediaQuery.addEventListener( 'change', ( event ) => { + isReducedMotion = event.matches; + } ); + return class SidebarSection extends customize.Section { ready() { const InspectorSection = getInspectorSection(); @@ -58,10 +66,6 @@ export default function getSidebarSection() { this.contentContainer .closest( '.wp-full-overlay' ) .addClass( 'section-open' ); - this.contentContainer.one( 'transitionend', () => { - this.contentContainer.removeClass( 'busy' ); - args.completeCallback(); - } ); } else { this.contentContainer.addClass( [ 'busy', @@ -71,10 +75,20 @@ export default function getSidebarSection() { .closest( '.wp-full-overlay' ) .addClass( 'section-open' ); this.contentContainer.removeClass( 'open' ); - this.contentContainer.one( 'transitionend', () => { - this.contentContainer.removeClass( 'busy' ); - args.completeCallback(); - } ); + } + + const handleTransitionEnd = () => { + this.contentContainer.removeClass( 'busy' ); + args.completeCallback(); + }; + + if ( isReducedMotion ) { + handleTransitionEnd(); + } else { + this.contentContainer.one( + 'transitionend', + handleTransitionEnd + ); } } else { super.onChangeExpanded( expanded, args ); diff --git a/packages/e2e-test-utils-playwright/src/page/click-block-toolbar-button.js b/packages/e2e-test-utils-playwright/src/page/click-block-toolbar-button.js new file mode 100644 index 0000000000000..955040e4351a1 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/click-block-toolbar-button.js @@ -0,0 +1,16 @@ +/** + * Clicks a block toolbar button. + * + * @this {import('./').PageUtils} + * @param {string} label The text string of the button label. + */ +export async function clickBlockToolbarButton( label ) { + await this.showBlockToolbar(); + + const blockToolbar = this.page.locator( + 'role=toolbar[name="Block tools"i]' + ); + const button = blockToolbar.locator( `role=button[name="${ label }"]` ); + + await button.click(); +} diff --git a/packages/e2e-test-utils-playwright/src/page/index.ts b/packages/e2e-test-utils-playwright/src/page/index.ts index 2988bc444d5a5..b5d8eb0364ca4 100644 --- a/packages/e2e-test-utils-playwright/src/page/index.ts +++ b/packages/e2e-test-utils-playwright/src/page/index.ts @@ -6,8 +6,10 @@ import type { Browser, Page, BrowserContext } from '@playwright/test'; /** * Internal dependencies */ +import { clickBlockToolbarButton } from './click-block-toolbar-button'; import { getPageError } from './get-page-error'; import { isCurrentURL } from './is-current-url'; +import { showBlockToolbar } from './show-block-toolbar'; import { visitAdminPage } from './visit-admin-page'; class PageUtils { @@ -21,8 +23,10 @@ class PageUtils { this.browser = this.context.browser()!; } + clickBlockToolbarButton = clickBlockToolbarButton; getPageError = getPageError; isCurrentURL = isCurrentURL; + showBlockToolbar = showBlockToolbar; visitAdminPage = visitAdminPage; } diff --git a/packages/e2e-test-utils-playwright/src/page/show-block-toolbar.js b/packages/e2e-test-utils-playwright/src/page/show-block-toolbar.js new file mode 100644 index 0000000000000..6dbc89a1b4587 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/show-block-toolbar.js @@ -0,0 +1,15 @@ +/** + * The block toolbar is not always visible while typing. + * Call this function to reveal it. + * + * @this {import('./').PageUtils} + */ +export async function showBlockToolbar() { + // Move the mouse to disable the isTyping mode. We need at least three + // mousemove events for it to work across windows (iframe). With three + // moves, it's a guarantee that at least two will be in the same window. + // Two events are required for the flag to be unset. + await this.page.mouse.move( 50, 50 ); + await this.page.mouse.move( 75, 75 ); + await this.page.mouse.move( 100, 100 ); +} diff --git a/packages/e2e-test-utils-playwright/src/request/index.ts b/packages/e2e-test-utils-playwright/src/request/index.ts index dcd8a715ad746..97ed3bbd308ff 100644 --- a/packages/e2e-test-utils-playwright/src/request/index.ts +++ b/packages/e2e-test-utils-playwright/src/request/index.ts @@ -17,6 +17,7 @@ import { getPluginsMap, activatePlugin, deactivatePlugin } from './plugins'; import { activateTheme } from './themes'; import { deleteAllBlocks } from './blocks'; import { deleteAllPosts } from './posts'; +import { deleteAllWidgets, addWidgetBlock } from './widgets'; interface StorageState { cookies: Cookie[]; @@ -115,6 +116,8 @@ class RequestUtils { activateTheme = activateTheme; deleteAllBlocks = deleteAllBlocks; deleteAllPosts = deleteAllPosts; + deleteAllWidgets = deleteAllWidgets; + addWidgetBlock = addWidgetBlock; } export type { StorageState }; diff --git a/packages/e2e-test-utils-playwright/src/request/widgets.js b/packages/e2e-test-utils-playwright/src/request/widgets.js new file mode 100644 index 0000000000000..2b4432b92061b --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/widgets.js @@ -0,0 +1,65 @@ +/** + * Delete all the widgets in the widgets screen. + * + * @this {import('./index').RequestUtils} + */ +export async function deleteAllWidgets() { + const [ widgets, sidebars ] = await Promise.all( [ + this.rest( { path: '/wp/v2/widgets' } ), + this.rest( { path: '/wp/v2/sidebars' } ), + ] ); + + await this.batchRest( + widgets.map( ( widget ) => ( { + method: 'DELETE', + path: `/wp/v2/widgets/${ widget.id }?force=true`, + } ) ) + ); + + await this.batchRest( + sidebars.map( ( sidebar ) => ( { + method: 'POST', + path: `/wp/v2/sidebars/${ sidebar.id }`, + body: { id: sidebar.id, widgets: [] }, + } ) ) + ); +} + +/** + * Add a widget block to the widget area. + * + * @this {import('./index').RequestUtils} + * @param {string} serializedBlock The serialized content of the inserted block HTML. + * @param {string} widgetAreaId The ID of the widget area. + */ +export async function addWidgetBlock( serializedBlock, widgetAreaId ) { + const { id: blockId } = await this.rest( { + method: 'POST', + path: '/wp/v2/widgets', + data: { + id_base: 'block', + sidebar: widgetAreaId, + instance: { + raw: { content: serializedBlock }, + }, + }, + } ); + + const { widgets } = await this.rest( { + path: `/wp/v2/sidebars/${ widgetAreaId }`, + } ); + + const updatedWidgets = new Set( widgets ); + // Remove duplicate. + updatedWidgets.delete( blockId ); + // Add to last block. + updatedWidgets.add( blockId ); + + await this.rest( { + method: 'PUT', + path: `/wp/v2/sidebars/${ widgetAreaId }`, + data: { + widgets: [ ...updatedWidgets ], + }, + } ); +} diff --git a/packages/e2e-tests/specs/widgets/customizing-widgets.test.js b/packages/e2e-tests/specs/widgets/customizing-widgets.test.js deleted file mode 100644 index a3c422bb40b56..0000000000000 --- a/packages/e2e-tests/specs/widgets/customizing-widgets.test.js +++ /dev/null @@ -1,913 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalActivatePlugin as activatePlugin, - activateTheme, - __experimentalDeactivatePlugin as deactivatePlugin, - visitAdminPage, - showBlockToolbar, - clickBlockToolbarButton, - deleteAllWidgets, - createURL, - openTypographyToolsPanelMenu, -} from '@wordpress/e2e-test-utils'; - -/** - * External dependencies - */ -// eslint-disable-next-line no-restricted-imports -import { find, findAll } from 'puppeteer-testing-library'; - -describe( 'Widgets Customizer', () => { - beforeEach( async () => { - await deleteAllWidgets(); - await visitAdminPage( 'customize.php' ); - - // Disable welcome guide if it is enabled. - const isWelcomeGuideActive = await page.evaluate( - () => - !! wp.data - .select( 'core/preferences' ) - .get( 'core/customize-widgets', 'welcomeGuide' ) - ); - if ( isWelcomeGuideActive ) { - await page.evaluate( () => - wp.data - .dispatch( 'core/preferences' ) - .toggle( 'core/customize-widgets', 'welcomeGuide' ) - ); - } - } ); - - beforeAll( async () => { - // TODO: Ideally we can bundle our test theme directly in the repo. - await activateTheme( 'twentytwenty' ); - await deactivatePlugin( - 'gutenberg-test-plugin-disables-the-css-animations' - ); - // Disable the transition timing function to make it "snap". - // We can't disable all the transitions yet because of #32024. - await page.evaluateOnNewDocument( () => { - const style = document.createElement( 'style' ); - style.innerHTML = ` - * { - transition-timing-function: step-start !important; - animation-timing-function: step-start !important; - } - `; - window.addEventListener( 'DOMContentLoaded', () => { - document.head.appendChild( style ); - } ); - } ); - await activatePlugin( 'gutenberg-test-widgets' ); - } ); - - afterAll( async () => { - await activatePlugin( - 'gutenberg-test-plugin-disables-the-css-animations' - ); - await deactivatePlugin( 'gutenberg-test-widgets' ); - await activateTheme( 'twentytwentyone' ); - } ); - - it( 'should add blocks', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await waitForPreviewIframe(); - - await addBlock( 'Heading' ); - await page.keyboard.type( 'My Heading' ); - - const inlineAddBlockButton = await find( { - role: 'combobox', - name: 'Add block', - haspopup: 'menu', - } ); - await inlineAddBlockButton.click(); - - const inlineInserterSearchBox = await find( { - role: 'searchbox', - name: 'Search for blocks and patterns', - } ); - - await expect( inlineInserterSearchBox ).toHaveFocus(); - - await page.keyboard.type( 'Search' ); - - const searchOption = await find( { - role: 'option', - name: 'Search', - } ); - await searchOption.click(); - - const addedSearchBlock = await find( { - role: 'document', - name: 'Block: Search', - } ); - - const searchTitle = await find( - { - role: 'textbox', - name: 'Label text', - }, - { root: addedSearchBlock } - ); - await searchTitle.focus(); - - await page.keyboard.type( 'My ' ); - - await waitForPreviewIframe(); - - const findOptions = { - root: await find( { - name: 'Site Preview', - selector: 'iframe', - } ), - }; - - // Expect the paragraph to be found in the preview iframe. - await expect( { - text: 'First Paragraph', - selector: '.widget-content p', - } ).toBeFound( findOptions ); - - // Expect the heading to be found in the preview iframe. - await expect( { - role: 'heading', - name: 'My Heading', - selector: '.widget-content *', - } ).toBeFound( findOptions ); - - // Expect the search box to be found in the preview iframe. - await expect( { - role: 'searchbox', - name: 'My Search', - selector: '.widget-content *', - } ).toBeFound( findOptions ); - } ); - - it( 'should open the inspector panel', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await showBlockToolbar(); - await clickBlockToolbarButton( 'Options' ); - let showMoreSettingsButton = await find( { - role: 'menuitem', - name: 'Show more settings', - } ); - await showMoreSettingsButton.click(); - - const backButton = await find( { - role: 'button', - name: /Back/, - focused: true, - } ); - await expect( backButton ).toHaveFocus(); - - // Expect the inspector panel to be found. - let inspectorHeading = await find( { - role: 'heading', - name: 'Customizing ▸ Widgets ▸ Footer #1 Block Settings', - level: 3, - } ); - - // Expect the block title to be found. - await expect( { - role: 'heading', - name: 'Paragraph', - level: 2, - } ).toBeFound(); - - await backButton.click(); - - // Go back to the widgets editor. - await find( { - role: 'heading', - name: 'Customizing ▸ Widgets Footer #1', - level: 3, - } ); - - await expect( inspectorHeading ).not.toBeVisible(); - - await clickBlockToolbarButton( 'Options' ); - showMoreSettingsButton = await find( { - role: 'menuitem', - name: 'Show more settings', - } ); - await showMoreSettingsButton.click(); - - // Expect the inspector panel to be found. - inspectorHeading = await find( { - role: 'heading', - name: 'Customizing ▸ Widgets ▸ Footer #1 Block Settings', - level: 3, - } ); - - // Press Escape to close the inspector panel. - await page.keyboard.press( 'Escape' ); - - // Go back to the widgets editor. - await expect( { - role: 'heading', - name: 'Customizing ▸ Widgets Footer #1', - level: 3, - } ).toBeFound(); - - await expect( inspectorHeading ).not.toBeVisible(); - } ); - - it( 'should handle the inserter outer section', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - // We need to make some changes for the publish settings to appear. - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await waitForPreviewIframe(); - - const documentTools = await find( { - role: 'toolbar', - name: 'Document tools', - } ); - - // Open the inserter outer section. - const addBlockButton = await find( - { - role: 'button', - name: 'Add block', - }, - { root: documentTools } - ); - await addBlockButton.click(); - - // Expect the inserter outer section to be found. - await expect( { - role: 'heading', - name: 'Add a block', - level: 2, - } ).toBeFound(); - - // Expect to close the inserter outer section when pressing Escape. - await page.keyboard.press( 'Escape' ); - - // Open the inserter outer section again. - await addBlockButton.click(); - - // Expect the inserter outer section to be found again. - const inserterHeading = await find( { - role: 'heading', - name: 'Add a block', - level: 2, - } ); - - // Open the Publish Settings. - const publishSettingsButton = await find( { - role: 'button', - name: 'Publish Settings', - } ); - await publishSettingsButton.click(); - - // Expect the Publish Settings outer section to be found. - const publishSettings = await find( { - selector: '#sub-accordion-section-publish_settings', - } ); - - // Expect the inserter outer section to be closed. - await expect( inserterHeading ).not.toBeVisible(); - - // Focus the block and start typing to hide the block toolbar. - // Shouldn't be needed if we automatically hide the toolbar on blur. - const paragraphBlock = await find( { - role: 'document', - name: 'Paragraph block', - } ); - await paragraphBlock.focus(); - await page.keyboard.type( ' ' ); - - // Open the inserter outer section. - await addBlockButton.click(); - - await expect( { - role: 'heading', - name: 'Add a block', - level: 2, - } ).toBeFound(); - - // Expect the Publish Settings section to be closed. - await expect( publishSettings ).not.toBeVisible(); - - // Back to the widget areas panel. - const backButton = await find( { - role: 'button', - name: /Back/, - } ); - await backButton.click(); - - // Expect the inserter outer section to be closed. - await expect( { - role: 'heading', - name: 'Add a block', - level: 2, - } ).not.toBeFound(); - } ); - - it( 'should move focus to the block', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await waitForPreviewIframe(); - - await addBlock( 'Heading' ); - await page.keyboard.type( 'First Heading' ); - - // Navigate back to the parent panel. - const backButton = await find( { role: 'button', name: /Back/ } ); - await backButton.click(); - - await waitForPreviewIframe(); - - const iframe = await find( { - name: 'Site Preview', - selector: 'iframe', - } ); - - const paragraphWidget = await find( - { - text: /First Paragraph/, - selector: '.widget', - }, - { - root: iframe, - } - ); - - const editParagraphWidget = await find( - { - role: 'button', - name: 'Click to edit this widget.', - }, - { - root: paragraphWidget, - } - ); - await editParagraphWidget.click(); - - const firstParagraphBlock = await find( { - role: 'document', - name: 'Paragraph block', - text: 'First Paragraph', - } ); - await expect( firstParagraphBlock ).toHaveFocus(); - - // Expect to focus on a already focused widget. - await editParagraphWidget.click(); - await expect( firstParagraphBlock ).toHaveFocus(); - - const headingWidget = await find( - { - text: /First Heading/, - selector: '.widget', - }, - { - root: iframe, - } - ); - - const editHeadingWidget = await find( - { - role: 'button', - name: 'Click to edit this widget.', - }, - { - root: headingWidget, - } - ); - await editHeadingWidget.click(); - - const headingBlock = await find( { - role: 'document', - name: 'Block: Heading', - text: 'First Heading', - } ); - await expect( headingBlock ).toHaveFocus(); - } ); - - it( 'should clear block selection', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - const paragraphBlock = await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - await showBlockToolbar(); - - const sectionHeading = await find( { - role: 'heading', - name: 'Customizing ▸ Widgets Footer #1', - level: 3, - } ); - await sectionHeading.click(); - - // Expect clicking on the section title should clear the selection. - await expect( { - role: 'toolbar', - name: 'Block tools', - } ).not.toBeFound(); - - await paragraphBlock.focus(); - await showBlockToolbar(); - - const preview = await page.$( '#customize-preview' ); - await preview.click(); - - // Expect clicking on the preview iframe should clear the selection. - await expect( { - role: 'toolbar', - name: 'Block tools', - } ).not.toBeFound(); - - await paragraphBlock.focus(); - await showBlockToolbar(); - - const editorContainer = await page.$( - '#customize-control-sidebars_widgets-sidebar-1' - ); - const { x, y, width, height } = await editorContainer.boundingBox(); - // Simulate Clicking on the empty space at the end of the editor. - await page.mouse.click( x + width / 2, y + height + 10 ); - - // Expect clicking on the empty space at the end of the editor - // should clear the selection. - await expect( { - role: 'toolbar', - name: 'Block tools', - } ).not.toBeFound(); - } ); - - it( 'should handle legacy widgets', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - const legacyWidgetBlock = await addBlock( 'Legacy Widget' ); - const selectLegacyWidgets = await find( { - role: 'combobox', - name: 'Select a legacy widget to display:', - } ); - await selectLegacyWidgets.select( 'test_widget' ); - - await expect( { - role: 'heading', - name: 'Test Widget', - level: 3, - } ).toBeFound( { root: legacyWidgetBlock } ); - - let titleInput = await find( - { - role: 'textbox', - name: 'Title:', - }, - { - root: legacyWidgetBlock, - } - ); - - await titleInput.type( 'Hello Title' ); - - // Unfocus the current legacy widget. - await page.keyboard.press( 'Tab' ); - - // Disable reason: Sometimes the preview just doesn't fully load, - // it's the only way I know for now to ensure that the iframe is ready. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 2000 ); - await waitForPreviewIframe(); - - // Expect the legacy widget to show in the site preview frame. - await expect( { - role: 'heading', - name: 'Hello Title', - } ).toBeFound( { - root: await find( { - name: 'Site Preview', - selector: 'iframe', - } ), - } ); - - // Expect the preview in block to show when unfocusing the legacy widget block. - await expect( { - role: 'heading', - name: 'Hello Title', - } ).toBeFound( { - root: await find( { - selector: 'iframe', - name: 'Legacy Widget Preview', - } ), - } ); - - await legacyWidgetBlock.focus(); - await showBlockToolbar(); - - // Testing removing the block. - await clickBlockToolbarButton( 'Options' ); - const removeBlockButton = await find( { - role: 'menuitem', - name: /Remove Legacy Widget/, - } ); - await removeBlockButton.click(); - - // Add it back again using the variant. - const testWidgetBlock = await addBlock( 'Test Widget' ); - - titleInput = await find( - { - role: 'textbox', - name: 'Title:', - }, - { - root: testWidgetBlock, - } - ); - - await titleInput.type( 'Hello again!' ); - // Unfocus the current legacy widget. - await page.keyboard.press( 'Tab' ); - - // Expect the preview in block to show when unfocusing the legacy widget block. - await expect( { - role: 'heading', - name: 'Hello again!', - } ).toBeFound( { - root: await find( { - selector: 'iframe', - name: 'Legacy Widget Preview', - } ), - } ); - - const publishButton = await find( { - role: 'button', - name: 'Publish', - } ); - await publishButton.click(); - - // Wait for publishing to finish. - await page.waitForResponse( createURL( '/wp-admin/admin-ajax.php' ) ); - await expect( publishButton ).toMatchQuery( { - disabled: true, - } ); - - await page.goto( createURL( '/' ) ); - - // Expect the saved widgets to show on frontend. - await expect( { - role: 'heading', - name: 'Hello again!', - } ).toBeFound(); - } ); - - it( 'should handle esc key events', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - const paragraphBlock = await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - await showBlockToolbar(); - - // Open the more menu dropdown in block toolbar. - await clickBlockToolbarButton( 'Options' ); - await expect( { - role: 'menu', - name: 'Options', - } ).toBeFound(); - - // Expect pressing the Escape key to close the dropdown, - // but not close the editor. - await page.keyboard.press( 'Escape' ); - await expect( { - role: 'menu', - name: 'Options', - } ).not.toBeFound(); - await expect( paragraphBlock ).toBeVisible(); - - await paragraphBlock.focus(); - - // Expect pressing the Escape key to enter navigation mode, - // but not close the editor. - await page.keyboard.press( 'Escape' ); - await expect( { - text: /^You are currently in navigation mode\./, - selector: '*[aria-live="polite"][aria-relevant="additions text"]', - } ).toBeFound(); - await expect( paragraphBlock ).toBeVisible(); - } ); - - it( 'should move (inner) blocks to another sidebar', async () => { - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await showBlockToolbar(); - await clickBlockToolbarButton( 'Options' ); - const groupButton = await find( { - role: 'menuitem', - name: 'Group', - } ); - await groupButton.click(); - - // Refocus the paragraph block. - const paragraphBlock = await find( { - role: 'document', - name: 'Paragraph block', - value: 'First Paragraph', - } ); - await paragraphBlock.focus(); - await showBlockToolbar(); - await clickBlockToolbarButton( 'Move to widget area' ); - - const footer2Option = await find( { - role: 'menuitemradio', - name: 'Footer #2', - } ); - await footer2Option.click(); - - // Should switch to and expand Footer #2. - await expect( { - role: 'heading', - name: 'Customizing ▸ Widgets Footer #2', - } ).toBeFound(); - - // The paragraph block should be moved to the new sidebar and have focus. - const movedParagraphBlockQuery = { - role: 'document', - name: 'Paragraph block', - value: 'First Paragraph', - }; - await expect( movedParagraphBlockQuery ).toBeFound(); - const movedParagraphBlock = await find( movedParagraphBlockQuery ); - await expect( movedParagraphBlock ).toHaveFocus(); - } ); - - it( 'should not render Block Settings sections', async () => { - // We add Block Settings as a section, but it shouldn't display to - // the user as a section on the main menu. It's simply how we - // integrate the G sidebar inside the customizer. - const findAllBlockSettingsHeader = findAll( - { - role: 'heading', - name: /Block Settings/, - level: 3, - }, - { timeout: 0 } - ); - await expect( findAllBlockSettingsHeader ).toThrowQueryEmptyError(); - } ); - - it( 'should stay in block settings after making a change in that area', async () => { - // Open footer block widgets - const widgetsPanel = await find( { - role: 'heading', - name: /Widgets/, - level: 3, - } ); - await widgetsPanel.click(); - - const footer1Section = await find( { - role: 'heading', - name: /^Footer #1/, - level: 3, - } ); - await footer1Section.click(); - - // Add a block to make the publish button active. - await addBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await waitForPreviewIframe(); - - // Click Publish. - const publishButton = await find( { - role: 'button', - name: 'Publish', - } ); - await publishButton.click(); - - // Wait for publishing to finish. - await page.waitForResponse( createURL( '/wp-admin/admin-ajax.php' ) ); - await expect( publishButton ).toMatchQuery( { - disabled: true, - } ); - - // Select the paragraph block. - const paragraphBlock = await find( { - role: 'document', - name: 'Paragraph block', - } ); - await paragraphBlock.focus(); - - // Click the three dots button, then click "Show More Settings". - await showBlockToolbar(); - await clickBlockToolbarButton( 'Options' ); - const showMoreSettingsButton = await find( { - role: 'menuitem', - name: 'Show more settings', - } ); - await showMoreSettingsButton.click(); - - // Change `drop cap` (Any change made in this section is sufficient; not required to be `drop cap`). - await openTypographyToolsPanelMenu(); - await page.click( 'button[aria-label="Show Drop cap"]' ); - - const [ dropCapToggle ] = await page.$x( - "//label[contains(text(), 'Drop cap')]" - ); - await dropCapToggle.click(); - - // Now that we've made a change: - // (1) Publish button should be active - // (2) We should still be in the "Block Settings" area. - await find( { - role: 'button', - name: 'Publish', - } ); - - // This fails on 539cea09 and earlier; we get kicked back to the widgets area. - // We expect to stay in block settings. - await find( { - role: 'heading', - name: 'Customizing ▸ Widgets ▸ Footer #1 Block Settings', - level: 3, - } ); - } ); -} ); - -/** - * Wait when there's only one preview iframe. - * There could be a 2 iframes when it's changing from no widgets to - * adding a first widget to the sidebar, - */ -async function waitForPreviewIframe() { - await page.waitForFunction( - () => - document.querySelectorAll( '[name^="customize-preview-"]' ) - .length === 1 - ); -} - -async function addBlock( blockName ) { - const addBlockButton = await find( - { - role: 'button', - name: 'Add block', - }, - { - root: await find( { - role: 'toolbar', - name: 'Document tools', - } ), - } - ); - await addBlockButton.click(); - - const searchBox = await find( { - role: 'searchbox', - name: 'Search for blocks and patterns', - } ); - - // Clear the input. - await searchBox.evaluate( ( node ) => { - if ( node.value ) { - node.value = ''; - } - } ); - - // Click something so that the block toolbar, which sometimes obscures - // buttons in the inserter, goes away. - await searchBox.click(); - - await searchBox.type( blockName ); - - // TODO - remove this timeout when the test plugin for disabling CSS - // animations in tests works properly. - // - // This waits for the inserter panel animation to finish before - // attempting to insert a block. If the panel is still animating - // puppeteer can click on the wrong block. - // - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 300 ); - - const blockOption = await find( { - role: 'option', - name: blockName, - } ); - await blockOption.click(); - - const addedBlock = await find( { - role: 'document', - selector: '.is-selected[data-block]', - } ); - await addedBlock.focus(); - - return addedBlock; -} diff --git a/test/e2e/specs/sanity.spec.js b/test/e2e/specs/sanity.spec.js deleted file mode 100644 index 4f9464da854b2..0000000000000 --- a/test/e2e/specs/sanity.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); - -// Example sanity check test, should be removed once there are other tests. -test.describe( 'Sanity check', () => { - test( 'Expect site loaded', async ( { page, pageUtils } ) => { - await pageUtils.visitAdminPage( '/' ); - - await expect( page ).toHaveTitle( /Dashboard/ ); - } ); -} ); diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js new file mode 100644 index 0000000000000..fa08502c88208 --- /dev/null +++ b/test/e2e/specs/widgets/customizing-widgets.spec.js @@ -0,0 +1,649 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +/** + * @typedef {import('@playwright/test').Page} Page + * @typedef {import('@playwright/test').FrameLocator} FrameLocator + * @typedef {import('@wordpress/e2e-test-utils-playwright').PageUtils} PageUtils + * @typedef {import('@wordpress/e2e-test-utils-playwright').RequestUtils} RequestUtils + */ + +test.use( { + widgetsCustomizerPage: async ( { page, pageUtils }, use ) => { + await use( new WidgetsCustomizerPage( { page, pageUtils } ) ); + }, +} ); + +test.describe( 'Widgets Customizer', () => { + test.beforeAll( async ( { requestUtils } ) => { + // TODO: Ideally we can bundle our test theme directly in the repo. + await requestUtils.activateTheme( 'twentytwenty' ); + await requestUtils.activatePlugin( 'gutenberg-test-widgets' ); + } ); + + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllWidgets(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-widgets' ); + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'should add blocks', async ( { page, widgetsCustomizerPage } ) => { + const previewFrame = widgetsCustomizerPage.previewFrame; + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + await widgetsCustomizerPage.addBlock( 'Paragraph' ); + await page.keyboard.type( 'First Paragraph' ); + + // Expect the paragraph to be found in the preview iframe. + await expect( + previewFrame.locator( + 'css=.widget-content p >> text="First Paragraph"' + ) + ).toBeVisible(); + + await widgetsCustomizerPage.addBlock( 'Heading' ); + await page.keyboard.type( 'My Heading' ); + + // Expect the heading to be found in the preview iframe. + await expect( + previewFrame.locator( + 'css=.widget-content >> role=heading[name="My Heading"]' + ) + ).toBeVisible(); + + // Click on the inline appender. + await page.click( + 'css=.editor-styles-wrapper >> role=button[name="Add block"i]' + ); + + const inlineInserterSearchBox = page.locator( + 'role=searchbox[name="Search for blocks and patterns"i]' + ); + + await expect( inlineInserterSearchBox ).toBeFocused(); + + await page.keyboard.type( 'Search' ); + + await page.click( 'role=option[name="Search"i]' ); + + await page.focus( + 'role=document[name="Block: Search"i] >> role=textbox[name="Label text"i]' + ); + + await page.keyboard.type( 'My ' ); + + // Expect the search box to be found in the preview iframe. + await expect( + previewFrame.locator( + 'css=.widget-content >> role=searchbox[name="My Search"]' + ) + ).toHaveCount( 1 ); + } ); + + test( 'should open the inspector panel', async ( { + page, + pageUtils, + requestUtils, + widgetsCustomizerPage, + } ) => { + const showMoreSettingsButton = page.locator( + 'role=menuitem[name="Show more settings"i]' + ); + const backButton = page.locator( + 'role=button[name=/Back$/] >> visible=true' + ); + const inspectorHeading = page.locator( + 'role=heading[name="Customizing ▸ Widgets ▸ Footer #1 Block Settings"i][level=3]' + ); + const widgetsFooter1Heading = page.locator( + 'role=heading[name="Customizing ▸ Widgets Footer #1"i][level=3]' + ); + + await requestUtils.addWidgetBlock( + `\n

First Paragraph

\n`, + 'sidebar-1' + ); + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + await page.focus( 'text=First Paragraph' ); + await pageUtils.showBlockToolbar(); + await pageUtils.clickBlockToolbarButton( 'Options' ); + + await showMoreSettingsButton.click(); + + // The transition could take more time than 5 seconds. + await expect( backButton ).toHaveCount( 1, { timeout: 8000 } ); + await expect( backButton ).toBeFocused(); + + // Expect the inspector panel to be found. + await expect( inspectorHeading ).toBeVisible(); + + // Expect the block title to be found. + await expect( + page.locator( 'role=heading[name="Paragraph"i][level=2]' ) + ).toBeVisible(); + + // Go back to the widgets editor. + await backButton.click(); + await expect( widgetsFooter1Heading ).toBeVisible(); + await expect( inspectorHeading ).not.toBeVisible(); + + await pageUtils.clickBlockToolbarButton( 'Options' ); + await showMoreSettingsButton.click(); + + // Expect the inspector panel to be found. + await expect( inspectorHeading ).toBeVisible(); + + // Press Escape to close the inspector panel. + await page.keyboard.press( 'Escape' ); + + // Go back to the widgets editor. + await expect( widgetsFooter1Heading ).toBeVisible(); + + await expect( inspectorHeading ).not.toBeVisible(); + } ); + + test( 'should handle the inserter outer section', async ( { + page, + widgetsCustomizerPage, + } ) => { + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + // We need to make some changes for the publish settings to appear. + await widgetsCustomizerPage.addBlock( 'Paragraph' ); + await page.keyboard.type( 'First Paragraph' ); + + const addBlockButton = page.locator( + 'role=toolbar[name="Document tools"i] >> role=button[name="Add block"i]' + ); + const inserterHeading = page.locator( + 'role=heading[name="Add a block"i][level=2]' + ); + + // Open the inserter outer section. + await addBlockButton.click(); + + // Expect the inserter outer section to be found. + await expect( inserterHeading ).toBeVisible(); + + // Expect to close the inserter outer section when pressing Escape. + await page.keyboard.press( 'Escape' ); + + // Open the inserter outer section again. + await addBlockButton.click(); + + // Expect the inserter outer section to be found again. + await expect( inserterHeading ).toBeVisible(); + + // Open the Publish Settings. + await page.click( 'role=button[name="Publish Settings"i]' ); + + // Expect the Publish Settings outer section to be found. + const publishSettings = page.locator( + '#sub-accordion-section-publish_settings' + ); + await expect( publishSettings ).toBeVisible(); + + // Expect the inserter outer section to be closed. + await expect( inserterHeading ).not.toBeVisible(); + + // Focus the block and start typing to hide the block toolbar. + // Shouldn't be needed if we automatically hide the toolbar on blur. + await page.focus( 'role=document[name="Paragraph block"i]' ); + await page.keyboard.type( ' ' ); + + // Open the inserter outer section. + await addBlockButton.click(); + + await expect( inserterHeading ).toBeVisible(); + + // Expect the Publish Settings section to be closed. + await expect( publishSettings ).toBeHidden(); + + // Back to the widget areas panel. + await page.click( 'role=button[name=/Back$/] >> visible=true' ); + + // Expect the inserter outer section to be closed. + await expect( inserterHeading ).not.toBeVisible(); + } ); + + test( 'should move focus to the block', async ( { + page, + requestUtils, + widgetsCustomizerPage, + } ) => { + await requestUtils.addWidgetBlock( + `\n

First Paragraph

\n`, + 'sidebar-1' + ); + await requestUtils.addWidgetBlock( + `\n

First Heading

\n`, + 'sidebar-1' + ); + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + // Navigate back to the parent panel. + await page.click( 'role=button[name=/Back$/] >> visible=true' ); + + const previewFrame = widgetsCustomizerPage.previewFrame; + + const paragraphWidget = previewFrame.locator( + '.widget:has-text("First Paragraph")' + ); + + const editParagraphWidget = paragraphWidget.locator( + 'role=button[name="Click to edit this widget."i]' + ); + await editParagraphWidget.click(); + + const firstParagraphBlock = page.locator( + 'role=document[name="Paragraph block"i] >> text="First Paragraph"' + ); + await expect( firstParagraphBlock ).toBeFocused(); + + // Expect to focus on a already focused widget. + await editParagraphWidget.click(); + await expect( firstParagraphBlock ).toBeFocused(); + + const headingWidget = previewFrame.locator( + '.widget:has-text("First Heading")' + ); + + const editHeadingWidget = headingWidget.locator( + 'role=button[name="Click to edit this widget."i]' + ); + await editHeadingWidget.click(); + + const headingBlock = page.locator( + 'role=document[name="Block: Heading"i] >> text="First Heading"' + ); + await expect( headingBlock ).toBeFocused(); + } ); + + test( 'should clear block selection', async ( { + page, + pageUtils, + requestUtils, + widgetsCustomizerPage, + } ) => { + await requestUtils.addWidgetBlock( + `\n

First Paragraph

\n`, + 'sidebar-1' + ); + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + const paragraphBlock = page.locator( 'text="First Paragraph"' ); + await paragraphBlock.focus(); + await pageUtils.showBlockToolbar(); + + const blockToolbar = page.locator( + 'role=toolbar[name="Block tools"i]' + ); + + // Expect clicking on the section title should clear the selection. + { + await page.click( + 'role=heading[name="Customizing ▸ Widgets Footer #1"i][level=3]' + ); + await expect( blockToolbar ).not.toBeVisible(); + + await paragraphBlock.focus(); + await pageUtils.showBlockToolbar(); + } + + // Expect clicking on the preview iframe should clear the selection. + { + await page.click( '#customize-preview' ); + await expect( blockToolbar ).not.toBeVisible(); + + await paragraphBlock.focus(); + await pageUtils.showBlockToolbar(); + } + + // Expect clicking on the empty space at the end of the editor + // should clear the selection. + { + const editorContainer = page.locator( + '#customize-control-sidebars_widgets-sidebar-1' + ); + const { x, y, width, height } = await editorContainer.boundingBox(); + // Simulate Clicking on the empty space at the end of the editor. + await page.mouse.click( x + width / 2, y + height + 10 ); + await expect( blockToolbar ).not.toBeVisible(); + } + } ); + + test( 'should handle legacy widgets', async ( { + page, + pageUtils, + widgetsCustomizerPage, + } ) => { + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + const legacyWidgetBlock = await widgetsCustomizerPage.addBlock( + 'Legacy Widget' + ); + await page + .locator( + 'role=combobox[name="Select a legacy widget to display:"i]' + ) + .selectOption( 'test_widget' ); + + await expect( + legacyWidgetBlock.locator( + 'role=heading[name="Test Widget"][level=3]' + ) + ).toBeVisible(); + + let titleInput = legacyWidgetBlock.locator( + 'role=textbox[name="Title:"i]' + ); + + await titleInput.type( 'Hello Title' ); + + // Unfocus the current legacy widget. + await page.keyboard.press( 'Tab' ); + + const previewFrame = widgetsCustomizerPage.previewFrame; + const legacyWidgetPreviewFrame = page.frameLocator( + 'iframe[title="Legacy Widget Preview"]' + ); + + // Expect the legacy widget to show in the site preview frame. + await expect( + previewFrame.locator( 'role=heading[name="Hello Title"]' ) + ).toBeVisible(); + + // Expect the preview in block to show when unfocusing the legacy widget block. + await expect( + legacyWidgetPreviewFrame.locator( + 'role=heading[name="Hello Title"]' + ) + ).toBeVisible(); + + await legacyWidgetBlock.focus(); + await pageUtils.showBlockToolbar(); + + // Testing removing the block. + await pageUtils.clickBlockToolbarButton( 'Options' ); + await page.click( 'role=menuitem[name=/Remove Legacy Widget/]' ); + + // Add it back again using the variant. + const testWidgetBlock = await widgetsCustomizerPage.addBlock( + 'Test Widget' + ); + + titleInput = testWidgetBlock.locator( 'role=textbox[name="Title:"i]' ); + + await titleInput.type( 'Hello again!' ); + // Unfocus the current legacy widget. + await page.keyboard.press( 'Tab' ); + + // Expect the preview in block to show when unfocusing the legacy widget block. + await expect( + legacyWidgetPreviewFrame.locator( + 'role=heading[name="Hello again!"]' + ) + ).toBeVisible(); + + // Wait for publishing to finish. + await Promise.all( [ + page.waitForResponse( '/wp-admin/admin-ajax.php' ), + page.click( 'role=button[name="Publish"i]' ), + ] ); + await expect( + page.locator( 'role=button[name="Published"i]' ) + ).toBeDisabled(); + + await page.goto( '/' ); + + // Expect the saved widgets to show on frontend. + await expect( + page.locator( 'role=heading[name="Hello again!"]' ) + ).toBeVisible(); + } ); + + test( 'should handle esc key events', async ( { + page, + pageUtils, + requestUtils, + widgetsCustomizerPage, + } ) => { + await requestUtils.addWidgetBlock( + `\n

First Paragraph

\n`, + 'sidebar-1' + ); + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + const paragraphBlock = page.locator( 'text="First Paragraph"' ); + await paragraphBlock.focus(); + await pageUtils.showBlockToolbar(); + + const optionsMenu = page.locator( 'role=menu[name="Options"i]' ); + + // Open the more menu dropdown in block toolbar. + await pageUtils.clickBlockToolbarButton( 'Options' ); + await expect( optionsMenu ).toBeVisible(); + + // Expect pressing the Escape key to close the dropdown, + // but not close the editor. + await page.keyboard.press( 'Escape' ); + await expect( optionsMenu ).not.toBeVisible(); + await expect( paragraphBlock ).toBeVisible(); + + await paragraphBlock.focus(); + + // Expect pressing the Escape key to enter navigation mode, + // but not close the editor. + await page.keyboard.press( 'Escape' ); + await expect( + page.locator( + '*[aria-live="polite"][aria-relevant="additions text"] >> text=/^You are currently in navigation mode./' + ) + ).toHaveCount( 1 ); + await expect( paragraphBlock ).toBeVisible(); + } ); + + test( 'should move (inner) blocks to another sidebar', async ( { + page, + pageUtils, + requestUtils, + widgetsCustomizerPage, + } ) => { + await requestUtils.addWidgetBlock( + `\n

First Paragraph

\n`, + 'sidebar-1' + ); + + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + await page.focus( 'text="First Paragraph"' ); + await pageUtils.showBlockToolbar(); + await pageUtils.clickBlockToolbarButton( 'Options' ); + await page.click( 'role=menuitem[name="Group"i]' ); + + // Refocus the paragraph block. + await page.focus( + '*role=document[name="Paragraph block"i] >> text="First Paragraph"' + ); + await pageUtils.showBlockToolbar(); + await pageUtils.clickBlockToolbarButton( 'Move to widget area' ); + + await page.click( 'role=menuitemradio[name="Footer #2"i]' ); + + // Should switch to and expand Footer #2. + await expect( + page.locator( + 'role=heading[name="Customizing ▸ Widgets Footer #2"i]' + ) + ).toBeVisible(); + + // The paragraph block should be moved to the new sidebar and have focus. + const movedParagraphBlock = page.locator( + '*role=document[name="Paragraph block"i] >> text="First Paragraph"' + ); + await expect( movedParagraphBlock ).toBeVisible(); + await expect( movedParagraphBlock ).toBeFocused(); + } ); + + test( 'should not render Block Settings sections', async ( { + page, + widgetsCustomizerPage, + } ) => { + await widgetsCustomizerPage.visitCustomizerPage(); + + // We add Block Settings as a section, but it shouldn't display to + // the user as a section on the main menu. It's simply how we + // integrate the G sidebar inside the customizer. + await expect( + page.locator( 'role=heading[name=/Block Settings/][level=3]' ) + ).not.toBeVisible(); + } ); + + test( 'should stay in block settings after making a change in that area', async ( { + page, + pageUtils, + widgetsCustomizerPage, + } ) => { + await widgetsCustomizerPage.visitCustomizerPage(); + await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); + + // Add a block to make the publish button active. + await widgetsCustomizerPage.addBlock( 'Paragraph' ); + await page.keyboard.type( 'First Paragraph' ); + + // Click Publish + await Promise.all( [ + page.waitForResponse( '/wp-admin/admin-ajax.php' ), + page.click( 'role=button[name="Publish"i]' ), + ] ); + // Wait for publishing to finish. + await expect( + page.locator( 'role=button[name="Published"i]' ) + ).toBeDisabled(); + + // Select the paragraph block + await page.focus( 'role=document[name="Paragraph block"i]' ); + + // Click the three dots button, then click "Show More Settings". + await pageUtils.showBlockToolbar(); + await pageUtils.clickBlockToolbarButton( 'Options' ); + await page.click( 'role=menuitem[name="Show more settings"i]' ); + + // Change `drop cap` (Any change made in this section is sufficient; not required to be `drop cap`). + await page.click( + 'css=.typography-block-support-panel >> role=button[name=/^View( and add)? options$/]' + ); + await page.click( 'role=menuitemcheckbox[name="Show Drop cap"i]' ); + + await page.click( 'role=checkbox[name="Drop cap"i]' ); + + // Now that we've made a change: + // (1) Publish button should be active + // (2) We should still be in the "Block Settings" area + await expect( + page.locator( 'role=button[name="Publish"i]' ) + ).not.toBeDisabled(); + + // This fails on 539cea09 and earlier; we get kicked back to the widgets area. + // We expect to stay in block settings. + await expect( + page.locator( + 'role=heading[name="Customizing ▸ Widgets ▸ Footer #1 Block Settings"i][level=3]' + ) + ).toBeVisible(); + } ); +} ); + +class WidgetsCustomizerPage { + /** + * @param {Object} config + * @param {Page} config.page + * @param {PageUtils} config.pageUtils + */ + constructor( { page, pageUtils } ) { + this.page = page; + this.pageUtils = pageUtils; + + /** @type {FrameLocator} */ + this.previewFrame = this.page + .frameLocator( 'iframe[title="Site Preview"]' ) + // There could be two preview frames at the same time while updating. + // We only care about the latest (last) one. + .last(); + } + + async visitCustomizerPage() { + await this.pageUtils.visitAdminPage( 'customize.php' ); + + // Disable welcome guide. + await this.page.evaluate( () => { + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/customize-widgets', 'welcomeGuide', false ); + } ); + } + + /** + * @param {string} widgetAreaName The Widget Area's name to expand on. + */ + async expandWidgetArea( widgetAreaName ) { + await this.page.click( 'role=heading[name=/Widgets/i][level=3]' ); + + await this.page.click( + `role=heading[name=/${ widgetAreaName }/i][level=3]` + ); + } + + /** + * @param {string} blockName The block to be added. + */ + async addBlock( blockName ) { + await this.page.click( + 'role=toolbar[name="Document tools"i] >> role=button[name="Add block"i]' + ); + + const searchBox = this.page.locator( + 'role=searchbox[name="Search for blocks and patterns"i]' + ); + + // Clear the input. + await searchBox.evaluate( ( node ) => { + if ( node.value ) { + node.value = ''; + } + } ); + + await searchBox.type( blockName ); + + await this.page.click( `role=option[name="${ blockName }"]` ); + + const addedBlock = this.page.locator( + 'role=document >> css=.is-selected[data-block]' + ); + await addedBlock.focus(); + + const blockId = await addedBlock.getAttribute( 'data-block' ); + const stableSelector = `[data-block="${ blockId }"]`; + + return this.page.locator( stableSelector ); + } +}