From f472b30ddc71b8faf04e60decdee73f1951bdc26 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 13 Oct 2025 12:24:12 +0100 Subject: [PATCH 1/4] Fix Page List to Navigation Link conversion with entity bindings - Extract buildNavigationLinkEntityBinding function from useEntityBinding hook - Export function from navigation-link/shared for reuse - Update Page List conversion to add entity binding metadata - Update tests to verify binding structure is included - Use hardcoded test expectations with shared variable for maintainability Fixes #72245 --- .../src/navigation-link/shared/index.js | 5 +- .../shared/test/use-entity-binding.js | 15 ++++- .../shared/use-entity-binding.js | 29 +++++--- .../page-list/test/convert-to-links-modal.js | 67 +++++++++++++++++++ .../use-convert-to-navigation-links.js | 8 +++ 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/packages/block-library/src/navigation-link/shared/index.js b/packages/block-library/src/navigation-link/shared/index.js index f51728c8b7bfcd..9e1d52a73c2b09 100644 --- a/packages/block-library/src/navigation-link/shared/index.js +++ b/packages/block-library/src/navigation-link/shared/index.js @@ -7,5 +7,8 @@ export { Controls } from './controls'; export { updateAttributes } from './update-attributes'; -export { useEntityBinding } from './use-entity-binding'; +export { + useEntityBinding, + buildNavigationLinkEntityBinding, +} from './use-entity-binding'; export { LinkUI } from '../link-ui'; diff --git a/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js b/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js index 31636c9e8cb8ae..26b131bf7f8c77 100644 --- a/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js +++ b/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js @@ -10,7 +10,10 @@ import { renderHook, act } from '@testing-library/react'; /** * Internal dependencies */ -import { useEntityBinding } from '../use-entity-binding'; +import { + useEntityBinding, + buildNavigationLinkEntityBinding, +} from '../use-entity-binding'; // Mock the entire @wordpress/block-editor module jest.mock( '@wordpress/block-editor', () => ( { @@ -266,7 +269,15 @@ describe( 'useEntityBinding', () => { result.current.createBinding(); } ); - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { + expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( + buildNavigationLinkEntityBinding() + ); + } ); +} ); + +describe( 'buildNavigationLinkEntityBinding', () => { + it( 'should build the correct binding structure', () => { + expect( buildNavigationLinkEntityBinding() ).toEqual( { url: { source: 'core/post-data', args: { diff --git a/packages/block-library/src/navigation-link/shared/use-entity-binding.js b/packages/block-library/src/navigation-link/shared/use-entity-binding.js index 6e164f24955e71..5a48614ae5c0ea 100644 --- a/packages/block-library/src/navigation-link/shared/use-entity-binding.js +++ b/packages/block-library/src/navigation-link/shared/use-entity-binding.js @@ -4,6 +4,26 @@ import { useCallback } from '@wordpress/element'; import { useBlockBindingsUtils } from '@wordpress/block-editor'; +/** + * Builds entity binding configuration for navigation link URLs. + * This function generates the structure used to bind navigation link URLs to their entity sources. + * + * Using a function instead of a constant allows for future enhancements where the binding + * might need dynamic data (e.g., entity ID, context-specific arguments). + * + * @return {Object} Entity binding configuration object + */ +export function buildNavigationLinkEntityBinding() { + return { + url: { + source, + args: { + key: 'link', + }, + }, + }; +} + /** * Shared hook for entity binding functionality in Navigation blocks. * @@ -47,14 +67,7 @@ export function useEntityBinding( { clientId, attributes } ) { const source = kindToUse === 'taxonomy' ? 'core/term-data' : 'core/post-data'; - updateBlockBindings( { - url: { - source, - args: { - key: 'link', - }, - }, - } ); + updateBlockBindings( buildNavigationLinkEntityBinding() ); }, [ updateBlockBindings, kind, id ] ); diff --git a/packages/block-library/src/page-list/test/convert-to-links-modal.js b/packages/block-library/src/page-list/test/convert-to-links-modal.js index 985bed8381cfaf..420a7d1b000aa9 100644 --- a/packages/block-library/src/page-list/test/convert-to-links-modal.js +++ b/packages/block-library/src/page-list/test/convert-to-links-modal.js @@ -4,6 +4,16 @@ import { convertToNavigationLinks } from '../use-convert-to-navigation-links'; +// Expected entity binding structure for navigation links +const EXPECTED_ENTITY_BINDING = { + url: { + source: 'core/entity', + args: { + key: 'url', + }, + }, +}; + // Mock createBlock to avoid creating the blocks in test environment // as convertToNavigationLinks calls this method internally. jest.mock( '@wordpress/blocks', () => { @@ -117,6 +127,9 @@ describe( 'page list convert to links', () => { label: 'Sample Page', type: 'page', url: 'http://wordpress.local/sample-page/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -128,6 +141,9 @@ describe( 'page list convert to links', () => { label: 'About', type: 'page', url: 'http://wordpress.local/about/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -137,6 +153,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 1', type: 'page', url: 'http://wordpress.local/about/about-sub-1/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -148,6 +167,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 2', type: 'page', url: 'http://wordpress.local/about/about-sub-2/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -162,6 +184,9 @@ describe( 'page list convert to links', () => { label: 'Contact Page', type: 'page', url: 'http://wordpress.local/contact-page/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -173,6 +198,9 @@ describe( 'page list convert to links', () => { label: 'Test', type: 'page', url: 'http://wordpress.local/test/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -182,6 +210,9 @@ describe( 'page list convert to links', () => { label: 'Test Sub', type: 'page', url: 'http://wordpress.local/test/test-sub/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -191,6 +222,9 @@ describe( 'page list convert to links', () => { label: 'Test Sub Sub', type: 'page', url: 'http://wordpress.local/test/test-sub/test-sub-sub/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -297,6 +331,9 @@ describe( 'page list convert to links', () => { label: 'Sample Page', type: 'page', url: 'http://wordpress.local/sample-page/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -308,6 +345,9 @@ describe( 'page list convert to links', () => { label: 'About', type: 'page', url: 'http://wordpress.local/about/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -317,6 +357,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 1', type: 'page', url: 'http://wordpress.local/about/about-sub-1/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -328,6 +371,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 2', type: 'page', url: 'http://wordpress.local/about/about-sub-2/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -342,6 +388,9 @@ describe( 'page list convert to links', () => { label: 'Contact Page', type: 'page', url: 'http://wordpress.local/contact-page/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -353,6 +402,9 @@ describe( 'page list convert to links', () => { label: 'Test', type: 'page', url: 'http://wordpress.local/test/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -362,6 +414,9 @@ describe( 'page list convert to links', () => { label: 'Test Sub', type: 'page', url: 'http://wordpress.local/test/test-sub/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [ { @@ -371,6 +426,9 @@ describe( 'page list convert to links', () => { label: 'Test Sub Sub', type: 'page', url: 'http://wordpress.local/test/test-sub/test-sub-sub/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -481,6 +539,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 1', type: 'page', url: 'http://wordpress.local/about/about-sub-1/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -492,6 +553,9 @@ describe( 'page list convert to links', () => { label: 'About Sub 2', type: 'page', url: 'http://wordpress.local/about/about-sub-2/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', @@ -511,6 +575,9 @@ describe( 'page list convert to links', () => { label: 'Test Sub Sub', type: 'page', url: 'http://wordpress.local/test/test-sub/test-sub-sub/', + metadata: { + bindings: EXPECTED_ENTITY_BINDING, + }, }, innerBlocks: [], name: 'core/navigation-link', diff --git a/packages/block-library/src/page-list/use-convert-to-navigation-links.js b/packages/block-library/src/page-list/use-convert-to-navigation-links.js index 4cbc69d6e6de66..138bedd72df2b1 100644 --- a/packages/block-library/src/page-list/use-convert-to-navigation-links.js +++ b/packages/block-library/src/page-list/use-convert-to-navigation-links.js @@ -5,6 +5,11 @@ import { createBlock } from '@wordpress/blocks'; import { useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import { buildNavigationLinkEntityBinding } from '../navigation-link/shared'; + /** * Converts an array of pages into a nested array of navigation link blocks. * @@ -26,6 +31,9 @@ function createNavigationLinks( pages = [] ) { url, type, kind: 'post-type', + metadata: { + bindings: buildNavigationLinkEntityBinding(), + }, }, innerBlocks ); From f90bf64281e25e98e996f290e9d5a40e0d608e7d Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 17 Oct 2025 11:58:14 +0100 Subject: [PATCH 2/4] Refactor to accommodate changes from trunk --- .../shared/use-entity-binding.js | 14 +-- .../use-convert-to-navigation-links.js | 6 +- .../specs/editor/blocks/navigation.spec.js | 99 +++++++++++++++++++ 3 files changed, 110 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/navigation-link/shared/use-entity-binding.js b/packages/block-library/src/navigation-link/shared/use-entity-binding.js index 5a48614ae5c0ea..f9939ae273efe4 100644 --- a/packages/block-library/src/navigation-link/shared/use-entity-binding.js +++ b/packages/block-library/src/navigation-link/shared/use-entity-binding.js @@ -11,9 +11,12 @@ import { useBlockBindingsUtils } from '@wordpress/block-editor'; * Using a function instead of a constant allows for future enhancements where the binding * might need dynamic data (e.g., entity ID, context-specific arguments). * + * @param {string} kind - The kind of entity ('post-type' or 'taxonomy') * @return {Object} Entity binding configuration object */ -export function buildNavigationLinkEntityBinding() { +export function buildNavigationLinkEntityBinding( kind ) { + const source = kind === 'taxonomy' ? 'core/term-data' : 'core/post-data'; + return { url: { source, @@ -62,12 +65,9 @@ export function useEntityBinding( { clientId, attributes } ) { return; } - // Default to post-type in case there is a need to support dynamic kinds - // in the future. - const source = - kindToUse === 'taxonomy' ? 'core/term-data' : 'core/post-data'; - - updateBlockBindings( buildNavigationLinkEntityBinding() ); + updateBlockBindings( + buildNavigationLinkEntityBinding( kindToUse ) + ); }, [ updateBlockBindings, kind, id ] ); diff --git a/packages/block-library/src/page-list/use-convert-to-navigation-links.js b/packages/block-library/src/page-list/use-convert-to-navigation-links.js index 138bedd72df2b1..a32a334c29ef2c 100644 --- a/packages/block-library/src/page-list/use-convert-to-navigation-links.js +++ b/packages/block-library/src/page-list/use-convert-to-navigation-links.js @@ -18,6 +18,7 @@ import { buildNavigationLinkEntityBinding } from '../navigation-link/shared'; * @return {Array} A nested array of navigation link blocks. */ function createNavigationLinks( pages = [] ) { + const POST_TYPE_KIND = 'post-type'; const linkMap = {}; const navigationLinks = []; pages.forEach( ( { id, title, link: url, type, parent } ) => { @@ -30,9 +31,10 @@ function createNavigationLinks( pages = [] ) { label: title.rendered, url, type, - kind: 'post-type', + kind: POST_TYPE_KIND, metadata: { - bindings: buildNavigationLinkEntityBinding(), + bindings: + buildNavigationLinkEntityBinding( POST_TYPE_KIND ), }, }, innerBlocks diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index 01ee0223d88d66..da035949422dcd 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -1125,6 +1125,105 @@ test.describe( 'Navigation block', () => { await expect( linkInput ).toBeEnabled(); await expect( linkInput ).toHaveValue( testPage1.link ); } ); + + test( 'Page List converts to Navigation Links with entity bindings', async ( { + editor, + page, + admin, + requestUtils, + } ) => { + // Step 1: Create menu with Page List block + const menu = await requestUtils.createNavigationMenu( { + title: 'Test Menu with Page List', + content: '', + } ); + + // Step 2: Insert Navigation block + await admin.createNewPost(); + + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: menu.id, + }, + } ); + + // Step 3: Verify Page List is present + const pageListBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Page List', + } ); + await expect( pageListBlock ).toBeVisible(); + + // Verify pages are shown in the list + const pageItems = pageListBlock.locator( 'li' ); + + // Wait for Page List to load pages + await pageItems.first().waitFor( { state: 'visible' } ); + const itemCount = await pageItems.count(); + expect( itemCount ).toBeGreaterThan( 0 ); + + // Step 4: Convert Page List using Edit button + // Select the Page List block + await editor.selectBlocks( pageListBlock ); + + // Try using the toolbar Edit button instead + const editButton = page + .getByRole( 'button', { name: 'Edit' } ) + .first(); + await expect( editButton ).toBeVisible(); + + await editButton.click(); + + // Wait for modal and approve conversion + await expect( + page.getByRole( 'dialog', { name: 'Edit Page List' } ) + ).toBeVisible(); + + await page.getByRole( 'button', { name: 'Edit' } ).last().click(); + + // Wait for conversion - check that Page List is gone + await expect( pageListBlock ).toBeHidden(); + + // Step 5: Verify conversion to entity links + + // Get Navigation block + const navBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Navigation', + } ); + + // Should have Navigation Link blocks + const navLinkBlocks = navBlock.getByRole( 'document', { + name: 'Block: Page Link', + } ); + + const linkCount = await navLinkBlocks.count(); + expect( linkCount ).toBeGreaterThan( 0 ); + + // Select first link and verify binding + const navLinkBlock = navLinkBlocks.first(); + await editor.selectBlocks( navLinkBlock ); + + // Open sidebar to check Link field + await editor.openDocumentSettingsSidebar(); + const settingsControls = page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'tabpanel', { name: 'Settings' } ); + + await expect( settingsControls ).toBeVisible(); + + // Verify Link field is disabled (indicating binding is active) + const linkInput = settingsControls.getByRole( 'textbox', { + name: 'Link', + description: 'Synced with the selected page', + } ); + + await expect( linkInput ).toBeDisabled(); + + // Verify help text is present + await expect( + settingsControls.getByText( 'Synced with the selected page.' ) + ).toBeVisible(); + } ); } ); } ); From 385e8231bfd5817c9ae20ea47753cd3e90692fc4 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 17 Oct 2025 12:50:56 +0100 Subject: [PATCH 3/4] Fix expected binding in tests and rename for clarity --- ...nvert-to-links-modal.js => convert-to-navigation-links.js} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/block-library/src/page-list/test/{convert-to-links-modal.js => convert-to-navigation-links.js} (99%) diff --git a/packages/block-library/src/page-list/test/convert-to-links-modal.js b/packages/block-library/src/page-list/test/convert-to-navigation-links.js similarity index 99% rename from packages/block-library/src/page-list/test/convert-to-links-modal.js rename to packages/block-library/src/page-list/test/convert-to-navigation-links.js index 420a7d1b000aa9..a3cbdc8f184ad5 100644 --- a/packages/block-library/src/page-list/test/convert-to-links-modal.js +++ b/packages/block-library/src/page-list/test/convert-to-navigation-links.js @@ -7,9 +7,9 @@ import { convertToNavigationLinks } from '../use-convert-to-navigation-links'; // Expected entity binding structure for navigation links const EXPECTED_ENTITY_BINDING = { url: { - source: 'core/entity', + source: 'core/post-data', args: { - key: 'url', + key: 'link', }, }, }; From 1bfd369fa9fecd2976dcbc16b316ef65c6aaee18 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 17 Oct 2025 13:13:35 +0100 Subject: [PATCH 4/4] Improve invalid binding kind --- .../shared/test/use-entity-binding.js | 220 ++++-------------- .../shared/use-entity-binding.js | 33 ++- 2 files changed, 74 insertions(+), 179 deletions(-) diff --git a/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js b/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js index 26b131bf7f8c77..060e3eb715cdd5 100644 --- a/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js +++ b/packages/block-library/src/navigation-link/shared/test/use-entity-binding.js @@ -269,45 +269,9 @@ describe( 'useEntityBinding', () => { result.current.createBinding(); } ); - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( - buildNavigationLinkEntityBinding() - ); - } ); -} ); - -describe( 'buildNavigationLinkEntityBinding', () => { - it( 'should build the correct binding structure', () => { - expect( buildNavigationLinkEntityBinding() ).toEqual( { - url: { - source: 'core/post-data', - args: { - key: 'link', - }, - }, - } ); - } ); - - it( 'should create core/term-data binding when createBinding is called for taxonomy', () => { - const attributes = { - metadata: {}, - id: null, - kind: 'taxonomy', - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) - ); - - act( () => { - result.current.createBinding(); - } ); - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { url: { - source: 'core/term-data', + source: 'core/post-data', args: { key: 'link', }, @@ -315,132 +279,62 @@ describe( 'buildNavigationLinkEntityBinding', () => { } ); } ); - describe( 'clearBinding behavior', () => { - it( 'should call updateBlockBindings when clearBinding is called and valid binding exists', () => { - const attributes = { - metadata: { - bindings: { - url: { - source: 'core/post-data', - args: { key: 'link' }, - }, - }, + describe( 'buildNavigationLinkEntityBinding', () => { + it( 'returns correct binding for post-type', () => { + const binding = buildNavigationLinkEntityBinding( 'post-type' ); + expect( binding ).toEqual( { + url: { + source: 'core/post-data', + args: { key: 'link' }, }, - id: 123, - kind: 'post-type', - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) - ); - - act( () => { - result.current.clearBinding(); - } ); - - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { - url: undefined, } ); } ); - it( 'should call updateBlockBindings when clearBinding is called and valid taxonomy binding exists', () => { - const attributes = { - metadata: { - bindings: { - url: { - source: 'core/term-data', - args: { key: 'link' }, - }, - }, + it( 'returns correct binding for taxonomy', () => { + const binding = buildNavigationLinkEntityBinding( 'taxonomy' ); + expect( binding ).toEqual( { + url: { + source: 'core/term-data', + args: { key: 'link' }, }, - id: 456, - kind: 'taxonomy', - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) - ); - - act( () => { - result.current.clearBinding(); - } ); - - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { - url: undefined, } ); } ); - it( 'should NOT call updateBlockBindings when clearBinding is called and binding exists but no id', () => { - const attributes = { - metadata: { - bindings: { - url: { - source: 'core/post-data', - args: { key: 'link' }, - }, - }, - }, - id: null, - kind: 'post-type', - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) + it( 'throws error when called without parameter', () => { + expect( () => { + buildNavigationLinkEntityBinding(); + } ).toThrow( + 'buildNavigationLinkEntityBinding requires a kind parameter' ); - - act( () => { - result.current.clearBinding(); - } ); - - expect( mockUpdateBlockBindings ).not.toHaveBeenCalled(); } ); - it( 'should call updateBlockBindings when clearBinding is called and binding exists with any source', () => { - const attributes = { - metadata: { - bindings: { - url: { - source: 'core/post-data', - args: { key: 'link' }, - }, - }, - }, - id: 123, - kind: 'post-type', // Correct kind for post-data source - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) - ); + it( 'throws error for invalid kind', () => { + expect( () => { + buildNavigationLinkEntityBinding( 'invalid-kind' ); + } ).toThrow( 'Invalid kind "invalid-kind"' ); + } ); - act( () => { - result.current.clearBinding(); - } ); + it( 'throws error for null kind', () => { + expect( () => { + buildNavigationLinkEntityBinding( null ); + } ).toThrow( 'Invalid kind "null"' ); + } ); - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { - url: undefined, - } ); + it( 'throws error for empty string', () => { + expect( () => { + buildNavigationLinkEntityBinding( '' ); + } ).toThrow( 'Invalid kind ""' ); } ); - } ); - describe( 'createBinding behavior', () => { - it( 'should not create binding when createBinding is called without kind', () => { + it( 'handles invalid kind gracefully in createBinding', () => { + const consoleSpy = jest + .spyOn( console, 'warn' ) + .mockImplementation(); + const attributes = { metadata: {}, id: null, - kind: null, + kind: 'invalid-kind', }; const { result } = renderHook( () => @@ -454,39 +348,15 @@ describe( 'buildNavigationLinkEntityBinding', () => { result.current.createBinding(); } ); - expect( mockUpdateBlockBindings ).not.toHaveBeenCalled(); - } ); - - it( 'should create binding with updated attributes when createBinding is called with updatedAttributes', () => { - const attributes = { - metadata: {}, - id: null, - kind: 'post-type', - }; - - const updatedAttributes = { - kind: 'taxonomy', - }; - - const { result } = renderHook( () => - useEntityBinding( { - clientId: 'test-client-id', - attributes, - } ) + expect( consoleSpy ).toHaveBeenCalledWith( + 'Failed to create entity binding:', + expect.stringContaining( 'Invalid kind "invalid-kind"' ) ); - act( () => { - result.current.createBinding( updatedAttributes ); - } ); + // Should not call updateBlockBindings when validation fails + expect( mockUpdateBlockBindings ).not.toHaveBeenCalled(); - expect( mockUpdateBlockBindings ).toHaveBeenCalledWith( { - url: { - source: 'core/term-data', - args: { - key: 'link', - }, - }, - } ); + consoleSpy.mockRestore(); } ); } ); } ); diff --git a/packages/block-library/src/navigation-link/shared/use-entity-binding.js b/packages/block-library/src/navigation-link/shared/use-entity-binding.js index f9939ae273efe4..6dbbe8652bf8c2 100644 --- a/packages/block-library/src/navigation-link/shared/use-entity-binding.js +++ b/packages/block-library/src/navigation-link/shared/use-entity-binding.js @@ -11,10 +11,27 @@ import { useBlockBindingsUtils } from '@wordpress/block-editor'; * Using a function instead of a constant allows for future enhancements where the binding * might need dynamic data (e.g., entity ID, context-specific arguments). * - * @param {string} kind - The kind of entity ('post-type' or 'taxonomy') + * @param {('post-type'|'taxonomy')} kind - The kind of entity. Only 'post-type' and 'taxonomy' are supported. * @return {Object} Entity binding configuration object + * @throws {Error} If kind is not 'post-type' or 'taxonomy' */ export function buildNavigationLinkEntityBinding( kind ) { + // Validate kind parameter exists + if ( kind === undefined ) { + throw new Error( + 'buildNavigationLinkEntityBinding requires a kind parameter. ' + + 'Only "post-type" and "taxonomy" are supported.' + ); + } + + // Validate kind parameter value + if ( kind !== 'post-type' && kind !== 'taxonomy' ) { + throw new Error( + `Invalid kind "${ kind }" provided to buildNavigationLinkEntityBinding. ` + + `Only 'post-type' and 'taxonomy' are supported.` + ); + } + const source = kind === 'taxonomy' ? 'core/term-data' : 'core/post-data'; return { @@ -65,9 +82,17 @@ export function useEntityBinding( { clientId, attributes } ) { return; } - updateBlockBindings( - buildNavigationLinkEntityBinding( kindToUse ) - ); + try { + const binding = buildNavigationLinkEntityBinding( kindToUse ); + updateBlockBindings( binding ); + } catch ( error ) { + // eslint-disable-next-line no-console + console.warn( + 'Failed to create entity binding:', + error.message + ); + // Don't create binding if validation fails + } }, [ updateBlockBindings, kind, id ] );