343 lines
9.0 KiB
TypeScript
343 lines
9.0 KiB
TypeScript
/**
|
||
* External dependencies
|
||
*/
|
||
import { Page } from '@playwright/test';
|
||
import { Editor } from '@wordpress/e2e-test-utils-playwright';
|
||
import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block';
|
||
|
||
export class EditorUtils {
|
||
editor: Editor;
|
||
page: Page;
|
||
constructor( editor: Editor, page: Page ) {
|
||
this.editor = editor;
|
||
this.page = page;
|
||
}
|
||
|
||
/**
|
||
* Check to see if there are any errors in the editor.
|
||
*/
|
||
async ensureNoErrorsOnBlockPage() {
|
||
const errorMessages = [
|
||
/This block contains unexpected or invalid content/gi,
|
||
/Your site doesn’t include support for/gi,
|
||
/There was an error whilst rendering/gi,
|
||
/This block has encountered an error and cannot be previewed/gi,
|
||
];
|
||
|
||
for ( const error of errorMessages ) {
|
||
if ( ( await this.editor.canvas.getByText( error ).count() ) > 0 ) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async getBlockByName( name: string ) {
|
||
return this.editor.canvas.locator( `[data-type="${ name }"]` );
|
||
}
|
||
|
||
async getBlockByTypeWithParent( name: string, parentName: string ) {
|
||
const parentBlock = await this.getBlockByName( parentName );
|
||
if ( ! parentBlock ) {
|
||
throw new Error( `Parent block "${ parentName }" not found.` );
|
||
}
|
||
const block = parentBlock.locator( `[data-type="${ name }"]` );
|
||
return block;
|
||
}
|
||
|
||
// todo: Make a PR to @wordpress/e2e-test-utils-playwright to add this method.
|
||
/**
|
||
* Inserts a block after a given client ID.
|
||
*
|
||
*/
|
||
async insertBlock(
|
||
blockRepresentation: BlockRepresentation,
|
||
index?: string,
|
||
rootClientId?: string
|
||
) {
|
||
await this.page.evaluate(
|
||
( {
|
||
blockRepresentation: _blockRepresentation,
|
||
index: _index,
|
||
rootClientId: _rootClientId,
|
||
} ) => {
|
||
function recursiveCreateBlock( {
|
||
name,
|
||
attributes = {},
|
||
innerBlocks = [],
|
||
}: BlockRepresentation ): BlockRepresentation {
|
||
return window.wp.blocks.createBlock(
|
||
name,
|
||
attributes,
|
||
innerBlocks.map( ( innerBlock ) =>
|
||
recursiveCreateBlock( innerBlock )
|
||
)
|
||
);
|
||
}
|
||
const block = recursiveCreateBlock( _blockRepresentation );
|
||
|
||
window.wp.data
|
||
.dispatch( 'core/block-editor' )
|
||
.insertBlock( block, _index, _rootClientId );
|
||
},
|
||
{ blockRepresentation, index, rootClientId }
|
||
);
|
||
}
|
||
|
||
async closeModalByName( name: string ) {
|
||
const isModalOpen = await this.page.getByLabel( name ).isVisible();
|
||
|
||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||
if ( isModalOpen ) {
|
||
await this.page
|
||
.getByLabel( name )
|
||
.getByRole( 'button', { name: 'Close' } )
|
||
.click();
|
||
}
|
||
}
|
||
async replaceBlockByBlockName( name: string, nameToInsert: string ) {
|
||
await this.page.evaluate(
|
||
( { name: _name, nameToInsert: _nameToInsert } ) => {
|
||
const blocks = window.wp.data
|
||
.select( 'core/block-editor' )
|
||
.getBlocks();
|
||
const firstMatchingBlock = blocks
|
||
.flatMap(
|
||
( {
|
||
innerBlocks,
|
||
}: {
|
||
innerBlocks: BlockRepresentation[];
|
||
} ) => innerBlocks
|
||
)
|
||
.find(
|
||
( block: BlockRepresentation ) => block.name === _name
|
||
);
|
||
const { clientId } = firstMatchingBlock;
|
||
const block = window.wp.blocks.createBlock( _nameToInsert );
|
||
window.wp.data
|
||
.dispatch( 'core/block-editor' )
|
||
.replaceBlock( clientId, block );
|
||
},
|
||
{ name, nameToInsert }
|
||
);
|
||
}
|
||
|
||
async getBlockRootClientId( clientId: string ) {
|
||
return this.page.evaluate< string | null, string >( ( id ) => {
|
||
return window.wp.data
|
||
.select( 'core/block-editor' )
|
||
.getBlockRootClientId( id );
|
||
}, clientId );
|
||
}
|
||
|
||
/**
|
||
* Toggles the global inserter.
|
||
*/
|
||
async toggleGlobalBlockInserter() {
|
||
// "Add block" selector is required to make sure performance comparison
|
||
// doesn't fail on older branches where we still had "Add block" as label.
|
||
await this.page.click(
|
||
'.edit-post-header [aria-label="Add block"],' +
|
||
'.edit-site-header [aria-label="Add block"],' +
|
||
'.edit-post-header [aria-label="Toggle block inserter"],' +
|
||
'.edit-site-header [aria-label="Toggle block inserter"],' +
|
||
'.edit-widgets-header [aria-label="Add block"],' +
|
||
'.edit-widgets-header [aria-label="Toggle block inserter"],' +
|
||
'.edit-site-header-edit-mode__inserter-toggle'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Checks if the global inserter is open.
|
||
*
|
||
* @return {Promise<boolean>} Whether the inserter is open or not.
|
||
*/
|
||
async isGlobalInserterOpen() {
|
||
return await this.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.
|
||
return !! document.querySelector(
|
||
'.edit-post-header [aria-label="Add block"].is-pressed,' +
|
||
'.edit-site-header-edit-mode [aria-label="Add block"].is-pressed,' +
|
||
'.edit-post-header [aria-label="Toggle block inserter"].is-pressed,' +
|
||
'.edit-site-header [aria-label="Toggle block inserter"].is-pressed,' +
|
||
'.edit-widgets-header [aria-label="Toggle block inserter"].is-pressed,' +
|
||
'.edit-widgets-header [aria-label="Add block"].is-pressed,' +
|
||
'.edit-site-header-edit-mode__inserter-toggle.is-pressed'
|
||
);
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Opens the global inserter.
|
||
*/
|
||
async openGlobalBlockInserter() {
|
||
if ( ! ( await this.isGlobalInserterOpen() ) ) {
|
||
await this.toggleGlobalBlockInserter();
|
||
await this.page.waitForSelector( '.block-editor-inserter__menu' );
|
||
}
|
||
}
|
||
|
||
async enterEditMode() {
|
||
await this.editor.page
|
||
.getByRole( 'button', {
|
||
name: 'Edit',
|
||
exact: true,
|
||
} )
|
||
.click();
|
||
}
|
||
|
||
async isBlockEarlierThan< T >(
|
||
containerBlock: T,
|
||
firstBlock: string,
|
||
secondBlock: string
|
||
) {
|
||
const container =
|
||
containerBlock instanceof Function
|
||
? await containerBlock()
|
||
: containerBlock;
|
||
|
||
if ( ! container ) {
|
||
throw new Error( 'Container block not found.' );
|
||
}
|
||
|
||
const childBlocks = container.locator( ':scope > .wp-block' );
|
||
|
||
let firstBlockIndex = -1;
|
||
let secondBlockIndex = -1;
|
||
|
||
for ( let i = 0; i < ( await childBlocks.count() ); i++ ) {
|
||
const blockName = await childBlocks
|
||
.nth( i )
|
||
.getAttribute( 'data-type' );
|
||
|
||
if ( blockName === firstBlock ) {
|
||
firstBlockIndex = i;
|
||
}
|
||
|
||
if ( blockName === secondBlock ) {
|
||
secondBlockIndex = i;
|
||
}
|
||
|
||
if ( firstBlockIndex !== -1 && secondBlockIndex !== -1 ) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ( firstBlockIndex === -1 || secondBlockIndex === -1 ) {
|
||
throw new Error( 'Both blocks must exist within the editor' );
|
||
}
|
||
|
||
return firstBlockIndex < secondBlockIndex;
|
||
}
|
||
|
||
async setLayoutOption(
|
||
option:
|
||
| 'Align Top'
|
||
| 'Align Bottom'
|
||
| 'Align Middle'
|
||
| 'Stretch to Fill'
|
||
) {
|
||
const button = this.page.locator(
|
||
"button[aria-label='Change vertical alignment']"
|
||
);
|
||
|
||
await button.click();
|
||
|
||
await this.page.getByText( option ).click();
|
||
}
|
||
|
||
async setAlignOption(
|
||
option: 'Align Left' | 'Align Center' | 'Align Right' | 'None'
|
||
) {
|
||
const button = this.page.locator( "button[aria-label='Align']" );
|
||
|
||
await button.click();
|
||
|
||
await this.page.getByText( option ).click();
|
||
}
|
||
|
||
async saveTemplate() {
|
||
await Promise.all( [
|
||
this.editor.saveSiteEditorEntities(),
|
||
this.editor.page.waitForResponse(
|
||
( response ) =>
|
||
response.url().includes( 'wp-json/wp/v2/templates/' ) ||
|
||
response.url().includes( 'wp-json/wp/v2/template-parts/' )
|
||
),
|
||
] );
|
||
}
|
||
|
||
async closeWelcomeGuideModal() {
|
||
const isModalOpen = await this.page
|
||
.getByRole( 'dialog', { name: 'Welcome to the site editor' } )
|
||
.locator( 'div' )
|
||
.filter( {
|
||
hasText:
|
||
'Edit your siteDesign everything on your site — from the header right down to the',
|
||
} )
|
||
.nth( 2 )
|
||
.isVisible();
|
||
|
||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||
if ( isModalOpen ) {
|
||
await this.page
|
||
.getByRole( 'button', { name: 'Get started' } )
|
||
.click();
|
||
}
|
||
}
|
||
|
||
async transformIntoBlocks() {
|
||
// Select the block, so the button is visible.
|
||
const block = this.page
|
||
.frameLocator( 'iframe[name="editor-canvas"]' )
|
||
.locator( `[data-type="woocommerce/legacy-template"]` )
|
||
.first();
|
||
|
||
if ( ! ( await block.isVisible() ) ) {
|
||
return;
|
||
}
|
||
|
||
await this.editor.selectBlocks( block );
|
||
|
||
const transformButton = block.getByRole( 'button', {
|
||
name: 'Transform into blocks',
|
||
} );
|
||
|
||
if ( transformButton ) {
|
||
await transformButton.click();
|
||
|
||
// save changes
|
||
await this.saveSiteEditorEntities();
|
||
}
|
||
}
|
||
|
||
// This method is the same as the one in @wordpress/e2e-test-utils-playwright. But for some reason
|
||
// it doesn't work as expected when imported from there. For its first run we get the following error:
|
||
// Error: locator.waitFor: Target closed
|
||
async saveSiteEditorEntities() {
|
||
const editorTopBar = this.page.getByRole( 'region', {
|
||
name: 'Editor top bar',
|
||
} );
|
||
const savePanel = this.page.getByRole( 'region', {
|
||
name: 'Save panel',
|
||
} );
|
||
|
||
// First Save button in the top bar.
|
||
await editorTopBar
|
||
.getByRole( 'button', { name: 'Save', exact: true } )
|
||
.click();
|
||
|
||
// Second Save button in the entities panel.
|
||
await savePanel
|
||
.getByRole( 'button', { name: 'Save', exact: true } )
|
||
.click();
|
||
|
||
await this.page
|
||
.getByRole( 'button', { name: 'Dismiss this notice' } )
|
||
.getByText( 'Site updated.' )
|
||
.waitFor();
|
||
}
|
||
}
|