diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 14289301e1c346..074cdd358266c8 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -12,7 +12,7 @@ import { getEditedPostContent } from './get-edited-post-content'; import { insertBlock } from './insert-block'; import { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; import { openPreviewPage } from './preview'; -import { selectBlockByClientId } from './select-block-by-client-id'; +import { selectBlocks } from './select-blocks'; import { showBlockToolbar } from './show-block-toolbar'; import { saveSiteEditorEntities } from './site-editor'; @@ -59,6 +59,6 @@ export class Editor { openDocumentSettingsSidebar = openDocumentSettingsSidebar; openPreviewPage = openPreviewPage; saveSiteEditorEntities = saveSiteEditorEntities; - selectBlockByClientId = selectBlockByClientId; + selectBlocks = selectBlocks; showBlockToolbar = showBlockToolbar; } diff --git a/packages/e2e-test-utils-playwright/src/editor/select-block-by-client-id.ts b/packages/e2e-test-utils-playwright/src/editor/select-block-by-client-id.ts deleted file mode 100644 index 4ca0e90688d2f3..00000000000000 --- a/packages/e2e-test-utils-playwright/src/editor/select-block-by-client-id.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Internal dependencies - */ -import type { Editor } from './index'; - -export async function selectBlockByClientId( this: Editor, clientId: string ) { - await this.page.evaluate( ( id: string ) => { - // @ts-ignore - wp.data.dispatch( 'core/block-editor' ).selectBlock( id ); - }, clientId ); -} diff --git a/packages/e2e-test-utils-playwright/src/editor/select-blocks.ts b/packages/e2e-test-utils-playwright/src/editor/select-blocks.ts new file mode 100644 index 00000000000000..8fba639e06858b --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/select-blocks.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { Locator } from '@playwright/test'; + +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +export async function selectBlocks( + this: Editor, + startSelectorOrLocator: string | Locator, + endSelectorOrLocator?: string | Locator +) { + const startBlock = + typeof startSelectorOrLocator === 'string' + ? this.canvas.locator( startSelectorOrLocator ) + : startSelectorOrLocator; + + const endBlock = + typeof endSelectorOrLocator === 'string' + ? this.canvas.locator( endSelectorOrLocator ) + : endSelectorOrLocator; + + const startClientId = await startBlock.getAttribute( 'data-block' ); + const endClientId = await endBlock?.getAttribute( 'data-block' ); + + if ( endClientId ) { + await this.page.evaluate( + ( [ startId, endId ] ) => { + // @ts-ignore + wp.data + .dispatch( 'core/block-editor' ) + .multiSelect( startId, endId ); + }, + [ startClientId, endClientId ] + ); + } else { + await this.page.evaluate( + ( [ clientId ] ) => { + // @ts-ignore + wp.data.dispatch( 'core/block-editor' ).selectBlock( clientId ); + }, + [ startClientId ] + ); + } +} diff --git a/packages/e2e-tests/specs/site-editor/template-part.test.js b/packages/e2e-tests/specs/site-editor/template-part.test.js deleted file mode 100644 index 0c29504136ac31..00000000000000 --- a/packages/e2e-tests/specs/site-editor/template-part.test.js +++ /dev/null @@ -1,346 +0,0 @@ -/** - * WordPress dependencies - */ -import { - insertBlock, - deleteAllTemplates, - activateTheme, - getAllBlocks, - selectBlockByClientId, - clickBlockToolbarButton, - canvas, - visitSiteEditor, -} from '@wordpress/e2e-test-utils'; - -const templatePartNameInput = - '.edit-site-create-template-part-modal .components-text-control__input'; - -describe( 'Template Part', () => { - beforeAll( async () => { - await activateTheme( 'emptytheme' ); - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - } ); - afterEach( async () => { - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - } ); - afterAll( async () => { - await activateTheme( 'twentytwentyone' ); - } ); - - describe( 'Template part block', () => { - beforeEach( async () => { - await visitSiteEditor(); - } ); - - async function navigateToHeader() { - // Switch to editing the header template part. - await visitSiteEditor( { - postId: 'emptytheme//header', - postType: 'wp_template_part', - } ); - } - - async function updateHeader( content ) { - await navigateToHeader(); - - // Edit it. - await insertBlock( 'Paragraph' ); - await page.keyboard.type( content ); - - // Save it. - await page.click( '.edit-site-save-button__button' ); - await page.click( '.editor-entities-saved-states__save-button' ); - await page.waitForSelector( - '.edit-site-save-button__button:not(.is-busy)' - ); - - // Switch back to the Index template. - await visitSiteEditor( { - postId: 'emptytheme//index', - postType: 'wp_template', - } ); - } - - async function triggerEllipsisMenuItem( textPrompt ) { - await clickBlockToolbarButton( 'Options' ); - const button = await page.waitForXPath( - `//span[contains(text(), "${ textPrompt }")]` - ); - await button.click(); - } - - async function createParagraphAndGetClientId( content ) { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( content ); - const allBlocks = await getAllBlocks(); - const paragraphBlock = allBlocks.find( - ( block ) => - block.name === 'core/paragraph' && - block.attributes.content === content - ); - return paragraphBlock.clientId; - } - - async function assertParagraphInTemplatePart( content ) { - const paragraphInTemplatePart = await canvas().waitForXPath( - `//*[@data-type="core/template-part"][//p[text()="${ content }"]]` - ); - expect( paragraphInTemplatePart ).not.toBeNull(); - } - - async function awaitHeaderLoad() { - await canvas().waitForSelector( - 'header.wp-block-template-part.block-editor-block-list__layout' - ); - } - - it( 'Should load customizations when in a template even if only the slug and theme attributes are set.', async () => { - await updateHeader( 'Header Template Part 123' ); - - // Verify that the header template part is updated. - await assertParagraphInTemplatePart( 'Header Template Part 123' ); - } ); - - it( 'Should detach blocks from template part', async () => { - await updateHeader( 'Header Template Part 456' ); - - const initialTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - - // Select the header template part block. - const allBlocks = await getAllBlocks(); - const headerBlock = allBlocks.find( - ( block ) => block.name === 'core/template-part' - ); - await selectBlockByClientId( headerBlock.clientId ); - - // Detach blocks from template part using ellipsis menu. - await triggerEllipsisMenuItem( 'Detach blocks from template part' ); - - // Verify there is one less template part on the page. - const finalTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - expect( - initialTemplateParts.length - finalTemplateParts.length - ).toBe( 1 ); - - // Verify content of the template part is still present. - const [ expectedContent ] = await canvas().$x( - '//p[contains(text(), "Header Template Part 456")]' - ); - expect( expectedContent ).not.toBeUndefined(); - } ); - - it( 'Should load navigate-to-links properly', async () => { - await navigateToHeader(); - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Header Template Part 789' ); - - // Select the paragraph block. - const text = await canvas().waitForXPath( - '//p[contains(text(), "Header Template Part 789")]' - ); - - // Highlight all the text in the paragraph block. - await text.click( { clickCount: 3 } ); - - // Click the convert to link toolbar button. - await page.waitForSelector( 'button[aria-label="Link"]' ); - await page.click( 'button[aria-label="Link"]' ); - - // Enter url for link. - await page.keyboard.type( 'https://google.com' ); - await page.keyboard.press( 'Enter' ); - - // Verify that there is no error. - await canvas().click( 'p[data-type="core/paragraph"] a' ); - const expectedContent = await canvas().$x( - '//p[contains(text(), "Header Template Part 789")]' - ); - - expect( expectedContent ).not.toBeUndefined(); - } ); - - it( 'Should convert selected block to template part', async () => { - await awaitHeaderLoad(); - const initialTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - - // Add some block and select it. - const clientId = await createParagraphAndGetClientId( - 'Some block...' - ); - await selectBlockByClientId( clientId ); - - // Convert block to a template part. - await triggerEllipsisMenuItem( 'Make template part' ); - const nameInput = await page.waitForSelector( - templatePartNameInput - ); - await nameInput.click(); - await page.keyboard.type( 'My template part' ); - await page.keyboard.press( 'Enter' ); - - // Wait for creation to finish. - await page.waitForXPath( - '//*[contains(@class, "components-snackbar")]/*[text()="Template part created."]' - ); - - // Verify new template part is created with expected content. - await assertParagraphInTemplatePart( 'Some block...' ); - - // Verify there is 1 more template part on the page than previously. - const finalTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - expect( - finalTemplateParts.length - initialTemplateParts.length - ).toBe( 1 ); - } ); - - it( 'Should convert multiple selected blocks to template part', async () => { - await awaitHeaderLoad(); - const initialTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - - // Add two blocks and select them. - const block1Id = await createParagraphAndGetClientId( - 'Some block #1' - ); - const block2Id = await createParagraphAndGetClientId( - 'Some block #2' - ); - await page.evaluate( - ( id1, id2 ) => { - wp.data - .dispatch( 'core/block-editor' ) - .multiSelect( id1, id2 ); - }, - block1Id, - block2Id - ); - - // Convert block to a template part. - await triggerEllipsisMenuItem( 'Make template part' ); - const nameInput = await page.waitForSelector( - templatePartNameInput - ); - await nameInput.click(); - await page.keyboard.type( 'My multi template part' ); - await page.keyboard.press( 'Enter' ); - - // Wait for creation to finish. - await page.waitForXPath( - '//*[contains(@class, "components-snackbar")]/*[text()="Template part created."]' - ); - - // Verify new template part is created with expected content. - await assertParagraphInTemplatePart( 'Some block #1' ); - await assertParagraphInTemplatePart( 'Some block #2' ); - - // Verify there is 1 more template part on the page than previously. - const finalTemplateParts = await canvas().$$( - '.wp-block-template-part' - ); - expect( - finalTemplateParts.length - initialTemplateParts.length - ).toBe( 1 ); - } ); - - describe( 'Template part placeholder', () => { - // Test constants for template part. - const testContent = 'some words...'; - - // Selectors. - const entitiesSaveSelector = - '.editor-entities-saved-states__save-button'; - const savePostSelector = '.edit-site-save-button__button'; - const templatePartSelector = '*[data-type="core/template-part"]'; - const activatedTemplatePartSelector = `${ templatePartSelector }.block-editor-block-list__layout`; - const startBlockButtonSelector = - '//button[contains(text(), "Start blank")]'; - const chooseExistingButtonSelector = - '//button[contains(text(), "Choose")]'; - const confirmTitleButtonSelector = - '.wp-block-template-part__placeholder-create-new__title-form .components-button.is-primary'; - - it( 'Should insert new template part on creation', async () => { - let siteEditorCanvas = canvas(); - await awaitHeaderLoad(); - - // Create new template part. - await insertBlock( 'Template Part' ); - const startBlankButton = await siteEditorCanvas.waitForXPath( - startBlockButtonSelector - ); - await startBlankButton.click(); - const confirmTitleButton = await page.waitForSelector( - confirmTitleButtonSelector - ); - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Tab' ); - await page.keyboard.type( 'Create New' ); - await confirmTitleButton.click(); - - const newTemplatePart = await siteEditorCanvas.waitForSelector( - activatedTemplatePartSelector - ); - expect( newTemplatePart ).toBeTruthy(); - - // Finish creating template part, insert some text, and save. - await siteEditorCanvas.waitForSelector( - '.block-editor-button-block-appender' - ); - await siteEditorCanvas.click( - '.block-editor-button-block-appender' - ); - await page.waitForSelector( - '.editor-block-list-item-paragraph' - ); - await page.click( '.editor-block-list-item-paragraph' ); - await page.keyboard.type( testContent ); - await page.click( savePostSelector ); - await page.click( entitiesSaveSelector ); - - // Reload the page so as the new template part is available in the existing template parts. - await visitSiteEditor(); - siteEditorCanvas = canvas(); - await awaitHeaderLoad(); - // Try to insert the template part we created. - await insertBlock( 'Template Part' ); - const chooseExistingButton = await siteEditorCanvas.waitForXPath( - chooseExistingButtonSelector - ); - await chooseExistingButton.click(); - const preview = await page.waitForSelector( - '.block-editor-block-patterns-list__item' - ); - await preview.click(); - - // Wait for the template parts to load properly. - await siteEditorCanvas.waitForSelector( - '[data-type="core/template-part"] > p:first-child' - ); - - // We now have the same template part two times in the page, so check accordingly. - const paragraphs = await siteEditorCanvas.$$eval( - '[data-type="core/template-part"] > p:first-child', - ( options ) => - options.map( ( option ) => option.textContent ) - ); - expect( paragraphs ).toHaveLength( 2 ); - expect( - paragraphs.every( - ( paragraph ) => paragraph === testContent - ) - ).toBeTruthy(); - } ); - } ); - } ); -} ); diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js new file mode 100644 index 00000000000000..e3be08c31b01e1 --- /dev/null +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -0,0 +1,303 @@ +/** + * WordPress dependencies + */ +const { + test, + expect, + Editor, +} = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + editor: async ( { page }, use ) => { + await use( new Editor( { page, hasIframe: true } ) ); + }, +} ); + +test.describe( 'Template Part', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'can create template parts via the block placeholder start blank option', async ( { + admin, + editor, + page, + } ) => { + await admin.visitSiteEditor( { + postId: 'emptytheme//header', + postType: 'wp_template_part', + } ); + + // Insert a new template block and 'start blank'. + await editor.insertBlock( { name: 'core/template-part' } ); + await editor.canvas.click( 'role=button[name="Start blank"i]' ); + + // Fill in a name in the dialog that pops up. + await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'New' ); + await page.keyboard.press( 'Enter' ); + + // The template part should be visible with a block appender. + const templatePart = editor.canvas.locator( + '[data-type="core/template-part"]' + ); + const addBlockButton = templatePart.locator( + 'role=button[name="Add block"i]' + ); + await expect( templatePart ).toBeVisible(); + await expect( addBlockButton ).toBeVisible(); + } ); + + test( 'can create template parts via the block placeholder choose existing option', async ( { + admin, + editor, + page, + } ) => { + // Visit the index. + await admin.visitSiteEditor(); + + const headerTemplateParts = editor.canvas.locator( + '[data-type="core/template-part"]' + ); + + // There should be 1 template part already in the index template. + await expect( headerTemplateParts ).toHaveCount( 1 ); + + // Insert a new template block and choose an existing header pattern. + await editor.insertBlock( { name: 'core/template-part' } ); + await editor.canvas.click( 'role=button[name="Choose"i]' ); + await page.click( + 'role=listbox[name="Block Patterns"i] >> role=option[name="header"i]' + ); + + // There are now two header template parts. + await expect( headerTemplateParts ).toHaveCount( 2 ); + } ); + + test( 'can convert a single block to a template part', async ( { + admin, + editor, + page, + } ) => { + const paragraphText = 'Test 2'; + + await admin.visitSiteEditor(); + + // Add a block and select it. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText, + }, + } ); + const paragraphBlock = editor.canvas.locator( + `p >> text="${ paragraphText }"` + ); + await editor.selectBlocks( paragraphBlock ); + + // Convert block to a template part. + await editor.clickBlockOptionsMenuItem( 'Make template part' ); + await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'Test' ); + await page.keyboard.press( 'Enter' ); + + await page.waitForSelector( + 'role=button >> text="Template part created."' + ); + + // Check that the header contains the paragraph added earlier. + const templatePartWithParagraph = editor.canvas.locator( + '[data-type="core/template-part"]', + { has: paragraphBlock } + ); + + await expect( templatePartWithParagraph ).toBeVisible(); + } ); + + test( 'can convert multiple blocks to a template part', async ( { + admin, + editor, + page, + } ) => { + const paragraphText1 = 'Test 3'; + const paragraphText2 = 'Test 4'; + + await admin.visitSiteEditor(); + + // Add a block and select it. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText1, + }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText2, + }, + } ); + const paragraphBlock1 = editor.canvas.locator( + `p >> text="${ paragraphText1 }"` + ); + const paragraphBlock2 = editor.canvas.locator( + `p >> text="${ paragraphText2 }"` + ); + + await editor.selectBlocks( paragraphBlock1, paragraphBlock2 ); + + // Convert block to a template part. + await editor.clickBlockOptionsMenuItem( 'Make template part' ); + await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'Test' ); + await page.keyboard.press( 'Enter' ); + + await page.waitForSelector( + 'role=button >> text="Template part created."' + ); + + // Check that the header contains the paragraph added earlier. + const templatePartWithParagraph1 = editor.canvas.locator( + '[data-type="core/template-part"]', + { has: paragraphBlock1 } + ); + const templatePartWithParagraph2 = editor.canvas.locator( + '[data-type="core/template-part"]', + { has: paragraphBlock2 } + ); + + // TODO: I couldn't find an easy way to assert that the same template + // part locator contains both paragraphs. It'd be nice to improve this. + await expect( templatePartWithParagraph1 ).toBeVisible(); + await expect( templatePartWithParagraph2 ).toBeVisible(); + await expect( templatePartWithParagraph1 ).toHaveText( + `${ paragraphText1 }${ paragraphText2 }` + ); + } ); + + test( 'can detach blocks from a template part', async ( { + admin, + editor, + } ) => { + const paragraphText = 'Test 3'; + + // Edit the header and save the changes. + await admin.visitSiteEditor( { + postId: 'emptytheme//header', + postType: 'wp_template_part', + } ); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText, + }, + } ); + await editor.saveSiteEditorEntities(); + + // Visit the index. + await admin.visitSiteEditor(); + + // Check that the header contains the paragraph added earlier. + const paragraph = editor.canvas.locator( + `p >> text="${ paragraphText }"` + ); + const templatePartWithParagraph = editor.canvas.locator( + '[data-type="core/template-part"]', + { has: paragraph } + ); + await expect( templatePartWithParagraph ).toBeVisible(); + + // Detach the paragraph from the header template part. + await editor.selectBlocks( templatePartWithParagraph ); + await editor.clickBlockOptionsMenuItem( + 'Detach blocks from template part' + ); + + // There should be a paragraph but no header template part. + await expect( paragraph ).toBeVisible(); + await expect( templatePartWithParagraph ).not.toBeVisible(); + } ); + + test( 'shows changes in a template when a template part it contains is modified', async ( { + admin, + editor, + } ) => { + const paragraphText = 'Test 1'; + + await admin.visitSiteEditor( { + postId: 'emptytheme//header', + postType: 'wp_template_part', + } ); + + // Edit the header. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText, + }, + } ); + + await editor.saveSiteEditorEntities(); + + // Visit the index. + await admin.visitSiteEditor(); + + const paragraph = editor.canvas.locator( + `p >> text="${ paragraphText }"` + ); + + await expect( paragraph ).toBeVisible(); + } ); + + // Tests for regressions of https://github.com/WordPress/gutenberg/pull/29239. + test( "doesn't throw a block error when clicking on a link", async ( { + admin, + editor, + page, + } ) => { + const paragraphText = 'Test 4'; + + await admin.visitSiteEditor( { + postId: 'emptytheme//header', + postType: 'wp_template_part', + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: paragraphText, + }, + } ); + + // Select the paragraph block. + const paragraph = editor.canvas.locator( + `p >> text="${ paragraphText }"` + ); + + // Highlight all the text in the paragraph block. + await paragraph.click( { clickCount: 3 } ); + + // Click the convert to link toolbar button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Enter url for link. + await page.keyboard.type( 'https://google.com' ); + await page.keyboard.press( 'Enter' ); + + // Verify that there is no error. + const paragraphLink = editor.canvas.locator( + `p >> a >> text="${ paragraphText }"` + ); + await paragraphLink.click( 'p[data-type="core/paragraph"] a' ); + + await expect( paragraph ).toBeVisible(); + } ); +} );