Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backport-changelog/6.8/8212.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/8212

* https://github.com/WordPress/gutenberg/pull/68926
* https://github.com/WordPress/gutenberg/pull/69142
* https://github.com/WordPress/gutenberg/pull/69241
30 changes: 30 additions & 0 deletions lib/compat/wordpress-6.8/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post
return apply_block_hooks_to_content( $content, $post, $callback );
}

/*
* If the content was created using the classic editor or using a single Classic block
* (`core/freeform`), it might not contain any block markup at all.
* However, we still might need to inject hooked blocks in the first child or last child
* positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap
* the content in a `core/freeform` wrapper block.
*/
if ( ! has_blocks( $content ) ) {
$original_content = $content;

$content_wrapped_in_classic_block = get_comment_delimited_block_content(
'core/freeform',
array(),
$content
);

$content = $content_wrapped_in_classic_block;
}

$attributes = array();

// If context is a post object, `ignoredHookedBlocks` information is stored in its post meta.
Expand Down Expand Up @@ -71,6 +90,17 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post
// Finally, we need to remove the temporary wrapper block.
$content = remove_serialized_parent_block( $content );

// If we wrapped the content in a `core/freeform` block, we also need to remove that.
if ( ! empty( $content_wrapped_in_classic_block ) ) {
/*
* We cannot simply use remove_serialized_parent_block() here,
* as that function assumes that the block wrapper is at the top level.
* However, there might now be a hooked block inserted next to it
* (as first or last child of the parent).
*/
$content = str_replace( $content_wrapped_in_classic_block, $original_content, $content );
}

return $content;
}
// We need to apply this filter before `do_blocks` (which is hooked to `the_content` at priority 9).
Expand Down
121 changes: 118 additions & 3 deletions test/e2e/specs/editor/plugins/block-hooks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

const dummyBlockContent = `<!-- wp:heading -->
const dummyBlocksContent = `<!-- 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 dummyClassicContent =
'<h2 class="dummy-heading">This is a dummy heading</h2><p class="dummy-paragraph">This is a dummy paragraph.</p>';

const getHookedBlockClassName = ( relativePosition, anchorBlock ) =>
`hooked-block-${ relativePosition }-${ anchorBlock.replace(
Expand All @@ -34,13 +36,13 @@ test.describe( 'Block Hooks API', () => {
createMethod: 'createBlock',
},
].forEach( ( { name, postType, blockType, createMethod } ) => {
test.describe( `Hooked blocks in ${ name }`, () => {
test.describe( `Hooked blocks in ${ name } (blocks)`, () => {
let postObject, containerPost;
test.beforeAll( async ( { requestUtils } ) => {
postObject = await requestUtils[ createMethod ]( {
title: name,
status: 'publish',
content: dummyBlockContent,
content: dummyBlocksContent,
} );

await requestUtils.activatePlugin(
Expand Down Expand Up @@ -162,6 +164,119 @@ test.describe( 'Block Hooks API', () => {
] );
} );
} );

test.describe( `Hooked blocks in ${ name } (classic)`, () => {
let postObject, containerPost;
test.beforeAll( async ( { requestUtils } ) => {
postObject = await requestUtils[ createMethod ]( {
title: name,
status: 'publish',
content: dummyClassicContent,
} );

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( [
'dummy-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 expectedHookedBlockLastChild = {
name: 'core/paragraph',
attributes: {
className: getHookedBlockClassName(
'last_child',
blockType
),
},
};

await admin.editPost( postObject.id );
await expect
.poll( editor.getBlocks )
.toMatchObject( [
{ name: 'core/freeform' },
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( [
expectedHookedBlockLastChild,
{ name: 'core/freeform' },
] );

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

test.describe( 'Hooked blocks in Navigation Menu', () => {
Expand Down
Loading