From e7895353c70ac597ed0727d83005ffe4d4aa04de Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 20 Mar 2026 11:42:41 +0100 Subject: [PATCH 1/7] Connectors: Support non-AI provider types and add JS extensibility e2e test Extends registerDefaultConnectors to handle connector types beyond ai_provider, adds an e2e test plugin demonstrating client-side connector registration via the merging strategy, and verifies the full flow in a new test case. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugins/connectors-js-extensibility.php | 52 +++++++++++++++++++ .../connectors-js-extensibility/index.mjs | 34 ++++++++++++ routes/connectors-home/default-connectors.tsx | 26 +++++----- test/e2e/specs/admin/connectors.spec.js | 42 +++++++++++++++ 4 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 packages/e2e-tests/plugins/connectors-js-extensibility.php create mode 100644 packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php new file mode 100644 index 00000000000000..f2f5d1ef357024 --- /dev/null +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -0,0 +1,52 @@ +register( + 'test_custom_service', + array( + 'name' => 'Test Custom Service', + 'description' => 'A custom service for E2E testing.', + 'type' => 'custom_service', + 'authentication' => array( + 'method' => 'none', + ), + ) + ); + } +); + +// Enqueue the script module on the connectors page. +add_action( + 'admin_enqueue_scripts', + static function () { + if ( ! isset( $_GET['page'] ) || 'options-connectors-wp-admin' !== $_GET['page'] ) { + return; + } + + wp_register_script_module( + 'gutenberg-test-connectors-js-extensibility', + plugins_url( 'connectors-js-extensibility/index.mjs', __FILE__ ), + array( + array( + 'id' => '@wordpress/connectors', + 'import' => 'static', + ), + ) + ); + wp_enqueue_script_module( '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 new file mode 100644 index 00000000000000..faa2e7bb7b331d --- /dev/null +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -0,0 +1,34 @@ +/** + * Script module that demonstrates client-side connector registration + * using the merging (upsert) strategy. + * + * Two registerConnector() calls target the same slug. The store + * shallow-merges each call, so the final connector combines the + * render function from one call with label/description from the other. + */ + +import { + __experimentalRegisterConnector as registerConnector, + __experimentalConnectorItem as ConnectorItem, +} from '@wordpress/connectors'; + +const h = window.React.createElement; + +// Register the render function for the connector. +registerConnector( 'test_custom_service', { + render: ( props ) => + h( + ConnectorItem, + { + className: 'connector-item--test_custom_service', + name: props.label, + description: props.description, + icon: props.icon, + }, + h( + 'p', + { className: 'test-custom-service-content' }, + 'Custom rendered content for testing.' + ) + ), +} ); diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index 6b41f270b4e369..ad428f783b19e9 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -231,28 +231,26 @@ function ApiKeyConnector( { export function registerDefaultConnectors() { const connectors = getConnectorData(); - const sanitize = ( s: string ) => s.replace( /[^a-z0-9-]/gi, '-' ); + const sanitize = ( s: string ) => s.replace( /[^a-z0-9-_]/gi, '-' ); for ( const [ connectorId, data ] of Object.entries( connectors ) ) { const { authentication } = data; - if ( - data.type !== 'ai_provider' || - authentication.method !== 'api_key' - ) { - continue; - } - - const connectorName = `${ sanitize( data.type ) }/${ sanitize( - connectorId - ) }`; - registerConnector( connectorName, { + const connectorName = sanitize( connectorId ); + const args: Partial< Omit< ConnectorConfig, 'slug' > > = { name: data.name, description: data.description, logo: getConnectorLogo( connectorId, data.logoUrl ), authentication, plugin: data.plugin, - render: ApiKeyConnector, - } ); + }; + if ( + data.type === 'ai_provider' && + authentication.method === 'api_key' + ) { + args.render = ApiKeyConnector; + } + + registerConnector( connectorName, args ); } } diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index c345046b02367c..59c2bd3b4c4cfa 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -518,4 +518,46 @@ test.describe( 'Connectors', () => { } ); } ); } ); + + test.describe( 'JS extensibility', () => { + const PLUGIN_SLUG = 'gutenberg-test-connectors-js-extensibility'; + + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( PLUGIN_SLUG ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( PLUGIN_SLUG ); + } ); + + test( 'should display a custom connector registered via JS with merging strategy', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONNECTORS_PAGE_QUERY + ); + + const card = page.locator( '.connector-item--test_custom_service' ); + await expect( card ).toBeVisible(); + + // Verify the custom content from the render function is visible. + await expect( + card.getByText( 'Custom rendered content for testing.' ) + ).toBeVisible(); + + // Verify label and description provided via a separate registerConnector + // call are merged into the same connector (upsert). + await expect( + card.getByRole( 'heading', { + name: 'Test Custom Service', + level: 2, + } ) + ).toBeVisible(); + await expect( + card.getByText( 'A custom service for E2E testing.' ) + ).toBeVisible(); + } ); + } ); } ); From a3ac192a01dc84ffed507d0d51d46fa4633d0297 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 20 Mar 2026 11:46:29 +0100 Subject: [PATCH 2/7] Tests: Add e2e coverage for server-only connector without JS render Registers a second connector (test_server_only_service) in the test plugin that has no client-side registerConnector call with a render function, and asserts that no card is displayed for it in the Connectors UI. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugins/connectors-js-extensibility.php | 12 +++++++++++ test/e2e/specs/admin/connectors.spec.js | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index f2f5d1ef357024..3c893e50eeedbc 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -26,6 +26,18 @@ static function ( WP_Connector_Registry $registry ) { ), ) ); + + $registry->register( + 'test_server_only_service', + array( + 'name' => 'Test Server Only Service', + 'description' => 'A server-only service with no JS render.', + 'type' => 'custom_service', + 'authentication' => array( + 'method' => 'none', + ), + ) + ); } ); diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index 59c2bd3b4c4cfa..bbb3db2ef0538c 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -530,6 +530,26 @@ test.describe( 'Connectors', () => { await requestUtils.deactivatePlugin( PLUGIN_SLUG ); } ); + test( 'should not display a card for a server-only connector without a JS render function', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( + SETTINGS_PAGE_PATH, + CONNECTORS_PAGE_QUERY + ); + + // The server registers test_server_only_service but no JS + // registerConnector call provides a render function for it, + // so no card should appear in the UI. + await expect( + page.getByRole( 'heading', { + name: 'Test Server Only Service', + level: 2, + } ) + ).toBeHidden(); + } ); + test( 'should display a custom connector registered via JS with merging strategy', async ( { page, admin, From 85c8cc0193c49aa71a426449d413d462ff051907 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 20 Mar 2026 11:47:33 +0100 Subject: [PATCH 3/7] Update plugin header to document both test connectors Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e-tests/plugins/connectors-js-extensibility.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index 3c893e50eeedbc..757558d580b3c9 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -4,9 +4,13 @@ * Plugin URI: https://github.com/WordPress/gutenberg * Author: Gutenberg Team * - * Registers a custom-type connector on the server and enqueues a script module - * that registers it client-side using the merging strategy (two registerConnector - * calls with the same slug: one providing the render function, the other metadata). + * Registers two custom-type 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. * * @package gutenberg-test-connectors-js-extensibility */ From c20277f04f06398a6a305347afe0ce1cd9d34f2d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 20 Mar 2026 11:52:09 +0100 Subject: [PATCH 4/7] Fix comments to accurately describe server+client merging behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e-tests/plugins/connectors-js-extensibility.php | 2 +- .../plugins/connectors-js-extensibility/index.mjs | 10 +++++----- test/e2e/specs/admin/connectors.spec.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility.php b/packages/e2e-tests/plugins/connectors-js-extensibility.php index 757558d580b3c9..8152a0d9171ff3 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility.php +++ b/packages/e2e-tests/plugins/connectors-js-extensibility.php @@ -15,7 +15,7 @@ * @package gutenberg-test-connectors-js-extensibility */ -// Register a non Ai provider connector which does not have UI component wired. +// Register two custom-type connectors for E2E testing. add_action( 'wp_connectors_init', static function ( WP_Connector_Registry $registry ) { diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index faa2e7bb7b331d..0d8df4bb335b78 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -1,10 +1,10 @@ /** - * Script module that demonstrates client-side connector registration - * using the merging (upsert) strategy. + * Script module that demonstrates client-side connector registration. * - * Two registerConnector() calls target the same slug. The store - * shallow-merges each call, so the final connector combines the - * render function from one call with label/description from the other. + * The server registers test_custom_service with its name and description. + * This module calls registerConnector() with the same slug to add a render + * function. The store merges both registrations, so the final connector + * combines the render function from JS with the metadata from PHP. */ import { diff --git a/test/e2e/specs/admin/connectors.spec.js b/test/e2e/specs/admin/connectors.spec.js index bbb3db2ef0538c..71c6c485ea9a37 100644 --- a/test/e2e/specs/admin/connectors.spec.js +++ b/test/e2e/specs/admin/connectors.spec.js @@ -567,8 +567,8 @@ test.describe( 'Connectors', () => { card.getByText( 'Custom rendered content for testing.' ) ).toBeVisible(); - // Verify label and description provided via a separate registerConnector - // call are merged into the same connector (upsert). + // Verify label and description from the server-side PHP registration + // are merged with the client-side JS render function. await expect( card.getByRole( 'heading', { name: 'Test Custom Service', From c8444e74dafa4ec1c26e5f1d80d618042d85da6d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 20 Mar 2026 12:12:10 +0100 Subject: [PATCH 5/7] Disable import/no-extraneous-dependencies for test plugin module Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index 0d8df4bb335b78..abc056fb774c65 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -7,6 +7,7 @@ * combines the render function from JS with the metadata from PHP. */ +// eslint-disable-next-line import/no-extraneous-dependencies import { __experimentalRegisterConnector as registerConnector, __experimentalConnectorItem as ConnectorItem, From 5156dd8555eef48cbbdead17f6c057758141889e Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Mon, 23 Mar 2026 11:11:51 +0100 Subject: [PATCH 6/7] Address PR feedback: hyphen support, type widening, and empty state fix Allow hyphens in connector IDs (aligning with wordpress-develop@b8e0c3df), widen ConnectorData.type from literal 'ai_provider' to string, and filter connectors by render function before checking empty state in stage.tsx. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/compat/wordpress-7.0/class-wp-connector-registry.php | 9 +++++---- routes/connectors-home/default-connectors.tsx | 2 +- routes/connectors-home/stage.tsx | 5 ++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/compat/wordpress-7.0/class-wp-connector-registry.php b/lib/compat/wordpress-7.0/class-wp-connector-registry.php index 27e5d6b385c38f..798e29942dee74 100644 --- a/lib/compat/wordpress-7.0/class-wp-connector-registry.php +++ b/lib/compat/wordpress-7.0/class-wp-connector-registry.php @@ -56,7 +56,7 @@ final class WP_Connector_Registry { * @since 7.0.0 * * @param string $id The unique connector identifier. Must contain only lowercase - * alphanumeric characters and underscores. + * alphanumeric characters, hyphens, and underscores. * @param array $args { * An associative array of arguments for the connector. * @@ -82,11 +82,11 @@ final class WP_Connector_Registry { * @phpstan-return Connector|null */ public function register( string $id, array $args ): ?array { - if ( ! preg_match( '/^[a-z0-9_]+$/', $id ) ) { + if ( ! preg_match( '/^[a-z0-9_-]+$/', $id ) ) { _doing_it_wrong( __METHOD__, __( - 'Connector ID must contain only lowercase alphanumeric characters and underscores.' + 'Connector ID must contain only lowercase alphanumeric characters, hyphens, and underscores.' ), '7.0.0' ); @@ -161,7 +161,8 @@ public function register( string $id, array $args ): ?array { if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) { $connector['authentication']['credentials_url'] = $args['authentication']['credentials_url']; } - $connector['authentication']['setting_name'] = "connectors_ai_{$id}_api_key"; + $sanitized_id = str_replace( '-', '_', $id ); + $connector['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key"; } if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) { diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index ad428f783b19e9..f69cdd26246b05 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -28,7 +28,7 @@ interface ConnectorData { name: string; description: string; logoUrl?: string; - type: 'ai_provider'; + type: string; plugin?: { slug: string; isInstalled: boolean; diff --git a/routes/connectors-home/stage.tsx b/routes/connectors-home/stage.tsx index b86929d59c4677..77a6f81ec8495f 100644 --- a/routes/connectors-home/stage.tsx +++ b/routes/connectors-home/stage.tsx @@ -42,7 +42,10 @@ function ConnectorsPage() { [] ); - const isEmpty = connectors.length === 0; + const renderableConnectors = connectors.filter( + ( connector: ConnectorConfig ) => connector.render + ); + const isEmpty = renderableConnectors.length === 0; return ( Date: Mon, 23 Mar 2026 11:19:37 +0100 Subject: [PATCH 7/7] Fix test plugin to use renamed connector props (name/logo) Update the e2e test plugin to use the renamed ConnectorRenderProps (name instead of label, logo instead of icon) from PR #76737. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e-tests/plugins/connectors-js-extensibility/index.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs index abc056fb774c65..8b96ef90b474d6 100644 --- a/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs +++ b/packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs @@ -22,9 +22,9 @@ registerConnector( 'test_custom_service', { ConnectorItem, { className: 'connector-item--test_custom_service', - name: props.label, + name: props.name, description: props.description, - icon: props.icon, + logo: props.logo, }, h( 'p',