Skip to content

Commit

Permalink
E2E Tests: Add Block Hooks Test Coverage (#69044)
Browse files Browse the repository at this point in the history
Add end-to-end test coverage for Block Hooks; specifically for insertion into post content, synced patterns, and Navigation blocks. The tests check that hooked blocks are inserted correctly on the frontend, and that any changes made in the editor are persisted and respected.

They cover both "normal" insertion (i.e. before or after a given anchor block), and first/last child insertion with the containing block serving as the anchor block (which is when the corresponding post object's post meta is used to store `_wp_ignored_hooked_blocks` data).

Co-authored-by: ockham <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
  • Loading branch information
4 people authored Feb 17, 2025
1 parent ed4df79 commit ce2a651
Show file tree
Hide file tree
Showing 2 changed files with 363 additions and 0 deletions.
74 changes: 74 additions & 0 deletions packages/e2e-tests/plugins/block-hooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
/**
* Plugin Name: Gutenberg Test Block Hooks
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-block-hooks
*/

defined( 'ABSPATH' ) || exit;

function gutenberg_test_insert_hooked_blocks( $hooked_blocks, $position, $anchor_block, $context ) {
if ( ! $context instanceof WP_Post ) {
return $hooked_blocks;
}

if (
( 'core/heading' === $anchor_block && 'after' === $position ) ||
( 'core/post-content' === $anchor_block && 'last_child' === $position ) ||
( 'core/block' === $anchor_block && 'last_child' === $position )
) {
$hooked_blocks[] = 'core/paragraph';
}

if ( 'core/navigation' === $anchor_block && 'first_child' === $position ) {
$hooked_blocks[] = 'core/home-link';
}

if ( 'core/navigation-link' === $anchor_block && 'after' === $position ) {
$hooked_blocks[] = 'core/page-list';
}

return $hooked_blocks;
}
add_filter( 'hooked_block_types', 'gutenberg_test_insert_hooked_blocks', 10, 4 );

function gutenberg_test_set_hooked_block_inner_html( $hooked_block, $hooked_block_type, $relative_position, $anchor_block ) {
if (
( 'core/heading' === $anchor_block['blockName'] && 'after' === $relative_position ) ||
( 'core/post-content' === $anchor_block['blockName'] && 'last_child' === $relative_position ) ||
( 'core/block' === $anchor_block['blockName'] && 'last_child' === $relative_position )
) {
$hooked_block['attrs'] = array(
'className' => "hooked-block-{$relative_position}-" . str_replace( 'core/', '', $anchor_block['blockName'] ),
);
$hooked_block['innerContent'] = array(
sprintf(
'<p class="%1$s">This block was inserted by the Block Hooks API in the <code>%2$s</code> position next to the <code>%3$s</code> anchor block.</p>',
$hooked_block['attrs']['className'],
$relative_position,
$anchor_block['blockName']
),
);
}

return $hooked_block;
}
add_filter( 'hooked_block_core/paragraph', 'gutenberg_test_set_hooked_block_inner_html', 10, 4 );

function gutenberg_register_wp_ignored_hooked_blocks_meta() {
register_post_meta(
'post',
'_wp_ignored_hooked_blocks',
array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
)
);
}
add_action( 'rest_api_init', 'gutenberg_register_wp_ignored_hooked_blocks_meta' );
289 changes: 289 additions & 0 deletions test/e2e/specs/editor/plugins/block-hooks.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

const dummyBlockContent = `<!-- wp:heading -->
<h2 class="wp-block-heading">This is a dummy heading</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"className":"dummy-paragraph"} -->
<p class="dummy-paragraph">This is a dummy paragraph.</p>
<!-- /wp:paragraph -->`;

const getHookedBlockClassName = ( relativePosition, anchorBlock ) =>
`hooked-block-${ relativePosition }-${ anchorBlock.replace(
'core/',
''
) }`;

const getHookedBlockContent = ( relativePosition, anchorBlock ) =>
`This block was inserted by the Block Hooks API in the ${ relativePosition } position next to the ${ anchorBlock } anchor block.`;

test.describe( 'Block Hooks API', () => {
[
{
name: 'Post Content',
postType: 'post',
blockType: 'core/post-content',
createMethod: 'createPost',
},
{
name: 'Synced Pattern',
postType: 'wp_block',
blockType: 'core/block',
createMethod: 'createBlock',
},
].forEach( ( { name, postType, blockType, createMethod } ) => {
test.describe( `Hooked blocks in ${ name }`, () => {
let postObject, containerPost;
test.beforeAll( async ( { requestUtils } ) => {
postObject = await requestUtils[ createMethod ]( {
title: name,
status: 'publish',
content: dummyBlockContent,
} );

await requestUtils.activatePlugin(
'gutenberg-test-block-hooks'
);

if ( postType !== 'post' ) {
// We need a container post to hold our block instance.
containerPost = await requestUtils.createPost( {
title: `Block Hooks in ${ name }`,
status: 'publish',
content: `<!-- wp:${ blockType } {"ref":${ postObject.id }} /-->`,
meta: {
// Prevent Block Hooks from injecting blocks into the container
// post content so they won't distract from the ones injected
// into the block instance.
_wp_ignored_hooked_blocks: '["core/paragraph"]',
},
} );
} else {
containerPost = postObject;
}
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.deactivatePlugin(
'gutenberg-test-block-hooks'
);

await requestUtils.deleteAllPosts();
await requestUtils.deleteAllBlocks();
} );

test( `should insert hooked blocks into ${ name } on frontend`, async ( {
page,
} ) => {
await page.goto( `/?p=${ containerPost.id }` );
await expect(
page.locator( '.entry-content > *' )
).toHaveClass( [
'wp-block-heading',
getHookedBlockClassName( 'after', 'core/heading' ),
'dummy-paragraph',
getHookedBlockClassName( 'last_child', blockType ),
] );
} );

test( `should insert hooked blocks into ${ name } in editor and respect changes made there`, async ( {
admin,
editor,
page,
} ) => {
const expectedHookedBlockAfterHeading = {
name: 'core/paragraph',
attributes: {
className: getHookedBlockClassName(
'after',
'core/heading'
),
},
};

const expectedHookedBlockLastChild = {
name: 'core/paragraph',
attributes: {
className: getHookedBlockClassName(
'last_child',
blockType
),
},
};

await admin.editPost( postObject.id );
await expect
.poll( editor.getBlocks )
.toMatchObject( [
{ name: 'core/heading' },
expectedHookedBlockAfterHeading,
{ name: 'core/paragraph' },
expectedHookedBlockLastChild,
] );

const hookedBlock = editor.canvas.getByText(
getHookedBlockContent( 'last_child', blockType )
);
await editor.selectBlocks( hookedBlock );
await editor.clickBlockToolbarButton( 'Move up' );

// Save updated post.
const saveButton = page
.getByRole( 'region', { name: 'Editor top bar' } )
.getByRole( 'button', { name: 'Save', exact: true } );
await saveButton.click();
await page
.getByRole( 'button', { name: 'Dismiss this notice' } )
.filter( { hasText: 'updated' } )
.waitFor();

// Reload and verify that the new position of the hooked block has been persisted.
await page.reload();
await expect
.poll( editor.getBlocks )
.toMatchObject( [
{ name: 'core/heading' },
expectedHookedBlockAfterHeading,
expectedHookedBlockLastChild,
{ name: 'core/paragraph' },
] );

// Verify that the frontend reflects the changes made in the editor.
await page.goto( `/?p=${ containerPost.id }` );
await expect(
page.locator( '.entry-content > *' )
).toHaveClass( [
'wp-block-heading',
getHookedBlockClassName( 'after', 'core/heading' ),
getHookedBlockClassName( 'last_child', blockType ),
'dummy-paragraph',
] );
} );
} );
} );

test.describe( 'Hooked blocks in Navigation Menu', () => {
let postObject, containerPost;
test.beforeAll( async ( { requestUtils } ) => {
postObject = await requestUtils.createNavigationMenu( {
title: 'Navigation Menu',
status: 'publish',
content:
'<!-- wp:navigation-link {"label":"wordpress.org","url":"https://wordpress.org","kind":"custom"} /-->',
} );

await requestUtils.activatePlugin( 'gutenberg-test-block-hooks' );

// We need a container to hold our Navigation block instance.
// We create a page (instead of a post) so that it will also
// populate the Page List block, which is one of the hooked blocks
// we use in our testing.
containerPost = await requestUtils.createPage( {
title: 'Block Hooks in Navigation Menu',
status: 'publish',
content: `<!-- wp:navigation {"ref":${ postObject.id }} /-->`,
} );
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.deactivatePlugin( 'gutenberg-test-block-hooks' );

await requestUtils.deleteAllPages();
await requestUtils.deleteAllMenus();
} );

test( 'should insert hooked blocks into Navigation Menu on frontend', async ( {
page,
} ) => {
await page.goto( `/?p=${ containerPost.id }` );
await expect(
page.locator( '.wp-block-navigation__container > *' )
).toHaveClass( [
'wp-block-navigation-item wp-block-home-link',
' wp-block-navigation-item wp-block-navigation-link',
'wp-block-page-list',
] );
} );

test( 'should insert hooked blocks into Navigation Menu in editor and respect changes made there', async ( {
admin,
editor,
page,
} ) => {
await admin.visitSiteEditor( {
postId: postObject.id,
postType: 'wp_navigation',
canvas: 'edit',
} );

// Since the Navigation block is a controlled block, we need
// to specify its client ID when calling `getBlocks`.
let navigationBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Navigation',
} );
let navigationClientId =
await navigationBlock.getAttribute( 'data-block' );

await expect
.poll( () =>
editor.getBlocks( {
clientId: navigationClientId,
} )
)
.toMatchObject( [
{ name: 'core/home-link' },
{ name: 'core/navigation-link' },
{ name: 'core/page-list' },
] );

const hookedBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Home Link',
} );
await editor.selectBlocks( hookedBlock );
await editor.clickBlockToolbarButton( 'Move right' );

// Save updated post.
const saveButton = page
.getByRole( 'region', { name: 'Editor top bar' } )
.getByRole( 'button', { name: 'Save', exact: true } );
await saveButton.click();
await page
.getByRole( 'button', { name: 'Dismiss this notice' } )
.filter( { hasText: 'updated' } )
.waitFor();

// Reload and verify that the new position of the hooked block has been persisted.
await page.reload();

navigationBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Navigation',
} );
navigationClientId =
await navigationBlock.getAttribute( 'data-block' );

await expect
.poll( () =>
editor.getBlocks( {
clientId: navigationClientId,
} )
)
.toMatchObject( [
{ name: 'core/navigation-link' },
{ name: 'core/home-link' },
{ name: 'core/page-list' },
] );

// Verify that the frontend reflects the changes made in the editor.
await page.goto( `/?p=${ containerPost.id }` );
await expect(
page.locator( '.wp-block-navigation__container > *' )
).toHaveClass( [
' wp-block-navigation-item wp-block-navigation-link',
'wp-block-navigation-item wp-block-home-link',
'wp-block-page-list',
] );
} );
} );
} );

0 comments on commit ce2a651

Please sign in to comment.