diff --git a/packages/js/src/ai-content-planner/initialize.js b/packages/js/src/ai-content-planner/initialize.js
index ae4a1241a05..18106711154 100644
--- a/packages/js/src/ai-content-planner/initialize.js
+++ b/packages/js/src/ai-content-planner/initialize.js
@@ -58,14 +58,17 @@ export function insertFirstParagraph( blocks, insertBlock, isBannerRendered ) {
export const ContentPlannerEditorPlugin = () => {
const hasInserted = useRef( false );
- const { isNewPost, postType, blocks, minPostsMet, isBannerRendered } = useSelect( select => {
+ const { isNewPost, postType, blocks, minPostsMet, isBannerRendered, hasBlockTemplate } = useSelect( select => {
const coreEditor = select( "core/editor" );
+ const currentPostType = coreEditor.getCurrentPostType();
+ const postTypeObject = select( "core" ).getPostType( currentPostType );
return {
isNewPost: coreEditor.isEditedPostNew(),
- postType: coreEditor.getCurrentPostType(),
+ postType: currentPostType,
blocks: select( "core/block-editor" ).getBlocks(),
minPostsMet: select( CONTENT_PLANNER_STORE ).selectIsMinPostsMet(),
isBannerRendered: select( CONTENT_PLANNER_STORE ).selectIsBannerRendered(),
+ hasBlockTemplate: Array.isArray( postTypeObject?.template ) && postTypeObject.template.length > 0,
};
}, [] );
const hasAiStore = useSelect( select => isObject( select( STORE_NAME_AI ) ), [] );
@@ -76,12 +79,12 @@ export const ContentPlannerEditorPlugin = () => {
useYoastMetaSync();
useEffect( () => {
- if ( hasInserted.current || ! isNewPost || postType !== "post" || ! minPostsMet ) {
+ if ( hasInserted.current || ! isNewPost || postType !== "post" || ! minPostsMet || hasBlockTemplate ) {
return;
}
hasInserted.current = insertFirstParagraph( blocks, insertBlock, isBannerRendered );
- }, [ blocks, isNewPost, postType, insertBlock, minPostsMet ] );
+ }, [ blocks, isNewPost, postType, insertBlock, minPostsMet, hasBlockTemplate ] );
if ( ! hasAiStore ) {
return null;
diff --git a/packages/js/tests/ai-content-planner/initialize.test.js b/packages/js/tests/ai-content-planner/initialize.test.js
index 376e8829e3c..e9e0136d92a 100644
--- a/packages/js/tests/ai-content-planner/initialize.test.js
+++ b/packages/js/tests/ai-content-planner/initialize.test.js
@@ -1,48 +1,36 @@
import { render } from "../test-utils";
-import { ContentPlannerEditorPlugin, registerInlineBanner } from "../../src/ai-content-planner/initialize";
+import { ContentPlannerEditorPlugin, insertFirstParagraph, registerInlineBanner } from "../../src/ai-content-planner/initialize";
import { addFilter } from "@wordpress/hooks";
const mockSelectHasAiGeneratorConsent = jest.fn( () => false );
const mockSelectIsMinPostsMet = jest.fn( () => false );
const mockSelectIsBannerRendered = jest.fn( () => false );
+const buildMockSelect = ( { postTypeTemplate = null } = {} ) => {
+ const storeMap = {
+ "yoast-seo/ai-generator": { selectHasAiGeneratorConsent: mockSelectHasAiGeneratorConsent },
+ "yoast-seo/content-planner": {
+ selectIsMinPostsMet: mockSelectIsMinPostsMet,
+ selectIsBannerRendered: mockSelectIsBannerRendered,
+ },
+ "core/editor": {
+ isEditedPostNew: () => false,
+ getCurrentPostType: () => "post",
+ getEditedPostAttribute: () => "",
+ },
+ "core/block-editor": { getBlocks: () => [] },
+ core: { getPostType: () => ( { template: postTypeTemplate } ) },
+ "yoast-seo/editor": { getSnippetEditorTemplates: () => ( { title: "", description: "" } ) },
+ };
+ return ( storeName ) => storeMap[ storeName ] ?? {};
+};
+
jest.mock( "@wordpress/data", () => ( {
dispatch: jest.fn( () => ( {
updateData: jest.fn(),
setFocusKeyword: jest.fn(),
} ) ),
- useSelect: jest.fn( ( mapSelect ) => {
- const mockSelect = ( storeName ) => {
- if ( storeName === "yoast-seo/ai-generator" ) {
- return {
- selectHasAiGeneratorConsent: mockSelectHasAiGeneratorConsent,
- };
- }
- if ( storeName === "yoast-seo/content-planner" ) {
- return {
- selectIsMinPostsMet: mockSelectIsMinPostsMet,
- selectIsBannerRendered: mockSelectIsBannerRendered,
- };
- }
- if ( storeName === "core/editor" ) {
- return {
- isEditedPostNew: () => false,
- getCurrentPostType: () => "post",
- getEditedPostAttribute: () => "",
- };
- }
- if ( storeName === "core/block-editor" ) {
- return {
- getBlocks: () => [],
- };
- }
- if ( storeName === "yoast-seo/editor" ) {
- return { getSnippetEditorTemplates: () => ( { title: "", description: "" } ) };
- }
- return {};
- };
- return mapSelect( mockSelect );
- } ),
+ useSelect: jest.fn( ( mapSelect ) => mapSelect( buildMockSelect() ) ),
useDispatch: jest.fn( () => ( {
insertBlock: jest.fn(),
updateData: jest.fn(),
@@ -107,35 +95,15 @@ describe( "ContentPlannerEditorPlugin", () => {
test( "renders null when the AI generator store is not registered", () => {
const { useSelect } = require( "@wordpress/data" );
- useSelect.mockImplementation( ( mapSelect ) => {
- const mockSelect = ( storeName ) => {
- if ( storeName === "yoast-seo/ai-generator" ) {
- // Unregistered store returns undefined.
- return undefined;
- }
- if ( storeName === "yoast-seo/content-planner" ) {
- return {
- selectIsMinPostsMet: () => false,
- selectIsBannerRendered: () => false,
- };
- }
- if ( storeName === "core/editor" ) {
- return {
- isEditedPostNew: () => false,
- getCurrentPostType: () => "post",
- getEditedPostAttribute: () => "",
- };
- }
- if ( storeName === "core/block-editor" ) {
- return { getBlocks: () => [] };
- }
- if ( storeName === "yoast-seo/editor" ) {
- return { getSnippetEditorTemplates: () => ( { title: "", description: "" } ) };
- }
- return {};
- };
- return mapSelect( mockSelect );
- } );
+ useSelect.mockImplementation( ( mapSelect ) => mapSelect( buildMockSelect() ) );
+
+ // Override only the ai-generator store to simulate it being absent.
+ useSelect.mockImplementation( ( mapSelect ) => mapSelect( ( storeName ) => {
+ if ( storeName === "yoast-seo/ai-generator" ) {
+ return undefined;
+ }
+ return buildMockSelect()( storeName );
+ } ) );
const { container } = render( );
expect( container.firstChild ).toBeNull();
@@ -145,34 +113,7 @@ describe( "ContentPlannerEditorPlugin", () => {
mockSelectHasAiGeneratorConsent.mockReturnValue( true );
const { useSelect } = require( "@wordpress/data" );
- useSelect.mockImplementation( ( mapSelect ) => {
- const mockSelect = ( storeName ) => {
- if ( storeName === "yoast-seo/ai-generator" ) {
- return { selectHasAiGeneratorConsent: mockSelectHasAiGeneratorConsent };
- }
- if ( storeName === "yoast-seo/content-planner" ) {
- return {
- selectIsMinPostsMet: () => false,
- selectIsBannerRendered: () => false,
- };
- }
- if ( storeName === "core/editor" ) {
- return {
- isEditedPostNew: () => false,
- getCurrentPostType: () => "post",
- getEditedPostAttribute: () => "",
- };
- }
- if ( storeName === "core/block-editor" ) {
- return { getBlocks: () => [] };
- }
- if ( storeName === "yoast-seo/editor" ) {
- return { getSnippetEditorTemplates: () => ( { title: "", description: "" } ) };
- }
- return {};
- };
- return mapSelect( mockSelect );
- } );
+ useSelect.mockImplementation( ( mapSelect ) => mapSelect( buildMockSelect() ) );
const { getByTestId } = render( );
expect( getByTestId( "app" ).dataset.hasConsent ).toBe( "true" );
@@ -188,6 +129,77 @@ describe( "ContentPlannerEditorPlugin", () => {
} );
} );
+describe( "insertFirstParagraph", () => {
+ const mockInsertBlock = jest.fn();
+
+ beforeEach( () => {
+ mockInsertBlock.mockClear();
+ } );
+
+ test( "returns true immediately when the banner is already rendered", () => {
+ const result = insertFirstParagraph( [], mockInsertBlock, true );
+
+ expect( result ).toBe( true );
+ expect( mockInsertBlock ).not.toHaveBeenCalled();
+ } );
+
+ test( "inserts a paragraph and returns false when the canvas is empty", () => {
+ const result = insertFirstParagraph( [], mockInsertBlock, false );
+
+ expect( mockInsertBlock ).toHaveBeenCalledTimes( 1 );
+ expect( result ).toBe( false );
+ } );
+
+ test( "returns true without inserting when the canvas already has a paragraph", () => {
+ const blocks = [ { name: "core/paragraph" } ];
+
+ const result = insertFirstParagraph( blocks, mockInsertBlock, false );
+
+ expect( mockInsertBlock ).not.toHaveBeenCalled();
+ expect( result ).toBe( true );
+ } );
+
+ test( "returns false without inserting when blocks exist but none is a paragraph", () => {
+ const blocks = [ { name: "core/heading" } ];
+
+ const result = insertFirstParagraph( blocks, mockInsertBlock, false );
+
+ expect( mockInsertBlock ).not.toHaveBeenCalled();
+ expect( result ).toBe( false );
+ } );
+} );
+
+describe( "ContentPlannerEditorPlugin — block template guard", () => {
+ const mockInsertBlock = jest.fn();
+
+ beforeEach( () => {
+ mockInsertBlock.mockClear();
+ } );
+
+ test( "does not insert a paragraph when the post type has a block template", () => {
+ const { useSelect, useDispatch } = require( "@wordpress/data" );
+
+ mockSelectIsMinPostsMet.mockReturnValue( true );
+ useDispatch.mockImplementation( () => ( { insertBlock: mockInsertBlock, updateData: jest.fn(), setFocusKeyword: jest.fn() } ) );
+ useSelect.mockImplementation( ( mapSelect ) => mapSelect( ( storeName ) => {
+ if ( storeName === "core/editor" ) {
+ return {
+ isEditedPostNew: () => true,
+ getCurrentPostType: () => "post",
+ getEditedPostAttribute: () => "",
+ };
+ }
+ return buildMockSelect( { postTypeTemplate: [ [ "core/group", {} ] ] } )( storeName );
+ } ) );
+
+ render( );
+
+ expect( mockInsertBlock ).not.toHaveBeenCalled();
+
+ mockSelectIsMinPostsMet.mockReturnValue( false );
+ } );
+} );
+
describe( "registerInlineBanner", () => {
beforeEach( () => {
addFilter.mockClear();