From d29e03971673bd3858053fddcecfce9aa83fc45e Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 7 Apr 2026 17:04:12 -0600 Subject: [PATCH 1/2] Connectors: don't clobber third-party custom render in registerDefaultConnectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `registerDefaultConnectors()` runs inside the dynamically-imported `routes/connectors-home/content` module, which means it executes after any third-party plugin script module that called `__experimentalRegisterConnector()` at top level (e.g. plugins enqueueing on `options-connectors-wp-admin_init`). The connectors store reducer spreads the new config over the existing entry, so the unconditional `args.render = ApiKeyConnector` for `api_key`-authenticated providers overwrites any custom render the plugin already supplied. PR #76722 added e2e coverage for the *server-then-client* direction (where the JS register correctly wins because it runs after the default register), but didn't cover the *client-then-server* direction — which is the natural flow for plugin script modules. Fix: read the existing entry from the connectors store and only set `args.render = ApiKeyConnector` when no render has been registered yet. The reducer's spread of the existing entry preserves the plugin's render while still merging the server-side metadata (logo, plugin install state, authentication config) on top. Adds a third connector `test_api_key_with_custom_render` to the `connectors-js-extensibility` test plugin and a corresponding e2e test that asserts the custom render content is visible and the default API Key form is absent — exercising the previously-uncovered direction. Closes #77115 --- .../plugins/connectors-js-extensibility.php | 19 +++++++++++++- .../connectors-js-extensibility/index.mjs | 24 +++++++++++++++++ routes/connectors-home/default-connectors.tsx | 17 +++++++++++- test/e2e/specs/admin/connectors.spec.js | 26 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index 8152a0d9171ff3..5e45c18fcac390 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -4,13 +4,17 @@ * Plugin URI: https://github.com/WordPress/gutenberg * Author: Gutenberg Team * - * Registers two custom-type connectors on the server: + * Registers three connectors on the server: * * 1. test_custom_service — also registered client-side via a script module using * the merging strategy (two registerConnector calls with the same slug: one * providing the render function, the other metadata). * 2. test_server_only_service — server-only, with no client-side render function, * so it should not display a card in the UI. + * 3. test_api_key_with_custom_render — an api_key connector whose JS render is + * registered *before* the page's `registerDefaultConnectors()` call runs. + * Regression test for the bug where the default ApiKeyConnector overwrote + * third-party custom renders for any api_key-authenticated connector. * * @package gutenberg-test-connectors-js-extensibility */ @@ -42,6 +46,19 @@ static function ( WP_Connector_Registry $registry ) { ), ) ); + + $registry->register( + 'test_api_key_with_custom_render', + array( + 'name' => 'Test API Key With Custom Render', + 'description' => 'An api_key connector with a JS-registered custom render.', + 'type' => 'ai_provider', + 'authentication' => array( + 'method' => 'api_key', + 'settingName' => 'test_api_key_with_custom_render_key', + ), + ) + ); } ); diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index 8b96ef90b474d6..68e02e12988698 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -33,3 +33,27 @@ registerConnector( 'test_custom_service', { ) ), } ); + +// Regression test: register a custom render for an api_key-authenticated +// connector. The page's `registerDefaultConnectors()` would otherwise +// overwrite this render with the generic ApiKeyConnector form because the +// store reducer spreads new config over existing entries. The fix in +// `routes/connectors-home/default-connectors.tsx` skips setting +// `args.render = ApiKeyConnector` when an existing render is present. +registerConnector( 'test_api_key_with_custom_render', { + render: ( props ) => + h( + ConnectorItem, + { + className: 'connector-item--test_api_key_with_custom_render', + name: props.name, + description: props.description, + logo: props.logo, + }, + h( + 'p', + { className: 'test-api-key-with-custom-render-content' }, + 'Custom render survived registerDefaultConnectors().' + ) + ), +} ); diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 7e212ba3846bea..0434979845b515 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -7,15 +7,18 @@ import { __experimentalRegisterConnector as registerConnector, __experimentalConnectorItem as ConnectorItem, __experimentalDefaultConnectorSettings as DefaultConnectorSettings, + privateApis as connectorsPrivateApis, type ConnectorConfig, type ConnectorRenderProps, } from '@wordpress/connectors'; +import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { Badge } from '@wordpress/ui'; /** * Internal dependencies */ +import { unlock } from '../lock-unlock'; import { useConnectorPlugin } from './use-connector-plugin'; import { OpenAILogo, @@ -25,6 +28,8 @@ import { DefaultConnectorLogo, } from './logos'; +const { store: connectorsStore } = unlock( connectorsPrivateApis ); + interface ConnectorData { name: string; description: string; @@ -256,7 +261,17 @@ export function registerDefaultConnectors() { authentication, plugin: data.plugin, }; - if ( authentication.method === 'api_key' ) { + + // If a third-party plugin's script module already registered a + // custom render for this slug (which can happen when the plugin's + // connector bundle executes before this module's dynamic import + // chain settles), don't overwrite it. The reducer spreads the + // existing entry first, so leaving `render` out of `args` here + // preserves the plugin's render while still merging the + // server-side metadata (logo, plugin install state, etc.). + const existing = + unlock( select( connectorsStore ) ).getConnector( connectorName ); + if ( authentication.method === 'api_key' && ! existing?.render ) { args.render = ApiKeyConnector; } diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index a9ce1a202c588f..562528532de89c 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -581,5 +581,31 @@ test.describe( 'Connectors', () => { card.getByText( 'A custom service for E2E testing.' ) ).toBeVisible(); } ); + + test( 'should preserve a custom render for an api_key connector registered before registerDefaultConnectors', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONNECTORS_PAGE_QUERY + ); + + const card = page.locator( + '.connector-item--test_api_key_with_custom_render' + ); + await expect( card ).toBeVisible(); + + // The custom render content must be visible — proving the + // JS-supplied render survived registerDefaultConnectors(). + await expect( + card.getByText( + 'Custom render survived registerDefaultConnectors().' + ) + ).toBeVisible(); + + // And the default API key form must NOT be present in this card. + await expect( card.getByLabel( 'API Key' ) ).toHaveCount( 0 ); + } ); } ); } ); From b57b27147c9e59106d054d508f6da68035f554a5 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 10 Apr 2026 11:15:57 +0200 Subject: [PATCH 2/2] Connectors: polish custom-render regression test and comments Fixes a Prettier violation in registerDefaultConnectors, tightens the e2e assertion so it targets the API key textbox role (rather than matching the card's own accessible name), and rewords the newly added comments to describe intent instead of neighboring implementation details that will evolve. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugins/connectors-js-extensibility.php | 7 +++---- .../plugins/connectors-js-extensibility/index.mjs | 8 ++------ routes/connectors-home/default-connectors.tsx | 15 ++++++--------- test/e2e/specs/admin/connectors.spec.js | 9 +++++---- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index 5e45c18fcac390..a822f0c8d5ede4 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -11,10 +11,9 @@ * providing the render function, the other metadata). * 2. test_server_only_service — server-only, with no client-side render function, * so it should not display a card in the UI. - * 3. test_api_key_with_custom_render — an api_key connector whose JS render is - * registered *before* the page's `registerDefaultConnectors()` call runs. - * Regression test for the bug where the default ApiKeyConnector overwrote - * third-party custom renders for any api_key-authenticated connector. + * 3. test_api_key_with_custom_render — an api_key connector whose JS render + * is registered before the default registrations run, used to verify that + * a subsequent default registration does not replace an existing render. * * @package gutenberg-test-connectors-js-extensibility */ diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index 68e02e12988698..037c896a5a3be9 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -34,12 +34,8 @@ registerConnector( 'test_custom_service', { ), } ); -// Regression test: register a custom render for an api_key-authenticated -// connector. The page's `registerDefaultConnectors()` would otherwise -// overwrite this render with the generic ApiKeyConnector form because the -// store reducer spreads new config over existing entries. The fix in -// `routes/connectors-home/default-connectors.tsx` skips setting -// `args.render = ApiKeyConnector` when an existing render is present. +// Registers a custom render for an api_key connector to verify that a +// subsequent default registration for the same slug does not replace it. registerConnector( 'test_api_key_with_custom_render', { render: ( props ) => h( diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 0434979845b515..ebb9b188301d33 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -262,15 +262,12 @@ export function registerDefaultConnectors() { plugin: data.plugin, }; - // If a third-party plugin's script module already registered a - // custom render for this slug (which can happen when the plugin's - // connector bundle executes before this module's dynamic import - // chain settles), don't overwrite it. The reducer spreads the - // existing entry first, so leaving `render` out of `args` here - // preserves the plugin's render while still merging the - // server-side metadata (logo, plugin install state, etc.). - const existing = - unlock( select( connectorsStore ) ).getConnector( connectorName ); + // Preserve a render that was already registered for this slug by + // another caller. Omitting `render` from `args` leaves the existing + // render in place while the server-side metadata still merges on top. + const existing = unlock( select( connectorsStore ) ).getConnector( + connectorName + ); if ( authentication.method === 'api_key' && ! existing?.render ) { args.render = ApiKeyConnector; } diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index 562528532de89c..0703af3dd790df 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -596,16 +596,17 @@ test.describe( 'Connectors', () => { ); await expect( card ).toBeVisible(); - // The custom render content must be visible — proving the - // JS-supplied render survived registerDefaultConnectors(). + // The JS-registered custom render must be visible inside the card. await expect( card.getByText( 'Custom render survived registerDefaultConnectors().' ) ).toBeVisible(); - // And the default API key form must NOT be present in this card. - await expect( card.getByLabel( 'API Key' ) ).toHaveCount( 0 ); + // The default API key input must not appear inside the card. + await expect( + card.getByRole( 'textbox', { name: 'API Key' } ) + ).toHaveCount( 0 ); } ); } ); } );