Skip to content

Commit c27ff43

Browse files
gziolojorgefilipecosta
authored andcommitted
Connectors: Support non-AI provider types and add JS extensibility e2e test (#76722)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * Update plugin header to document both test connectors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix comments to accurately describe server+client merging behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Disable import/no-extraneous-dependencies for test plugin module Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: gziolo <gziolo@git.wordpress.org> Co-authored-by: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
1 parent a0cab4e commit c27ff43

File tree

6 files changed

+187
-20
lines changed

6 files changed

+187
-20
lines changed

lib/compat/wordpress-7.0/class-wp-connector-registry.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class WP_Connector_Registry {
5656
* @since 7.0.0
5757
*
5858
* @param string $id The unique connector identifier. Must contain only lowercase
59-
* alphanumeric characters and underscores.
59+
* alphanumeric characters, hyphens, and underscores.
6060
* @param array $args {
6161
* An associative array of arguments for the connector.
6262
*
@@ -82,11 +82,11 @@ final class WP_Connector_Registry {
8282
* @phpstan-return Connector|null
8383
*/
8484
public function register( string $id, array $args ): ?array {
85-
if ( ! preg_match( '/^[a-z0-9_]+$/', $id ) ) {
85+
if ( ! preg_match( '/^[a-z0-9_-]+$/', $id ) ) {
8686
_doing_it_wrong(
8787
__METHOD__,
8888
__(
89-
'Connector ID must contain only lowercase alphanumeric characters and underscores.'
89+
'Connector ID must contain only lowercase alphanumeric characters, hyphens, and underscores.'
9090
),
9191
'7.0.0'
9292
);
@@ -161,7 +161,8 @@ public function register( string $id, array $args ): ?array {
161161
if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) {
162162
$connector['authentication']['credentials_url'] = $args['authentication']['credentials_url'];
163163
}
164-
$connector['authentication']['setting_name'] = "connectors_ai_{$id}_api_key";
164+
$sanitized_id = str_replace( '-', '_', $id );
165+
$connector['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key";
165166
}
166167

167168
if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
/**
3+
* Plugin Name: Gutenberg Test Connectors JS Extensibility
4+
* Plugin URI: https://github.com/WordPress/gutenberg
5+
* Author: Gutenberg Team
6+
*
7+
* Registers two custom-type connectors on the server:
8+
*
9+
* 1. test_custom_service — also registered client-side via a script module using
10+
* the merging strategy (two registerConnector calls with the same slug: one
11+
* providing the render function, the other metadata).
12+
* 2. test_server_only_service — server-only, with no client-side render function,
13+
* so it should not display a card in the UI.
14+
*
15+
* @package gutenberg-test-connectors-js-extensibility
16+
*/
17+
18+
// Register two custom-type connectors for E2E testing.
19+
add_action(
20+
'wp_connectors_init',
21+
static function ( WP_Connector_Registry $registry ) {
22+
$registry->register(
23+
'test_custom_service',
24+
array(
25+
'name' => 'Test Custom Service',
26+
'description' => 'A custom service for E2E testing.',
27+
'type' => 'custom_service',
28+
'authentication' => array(
29+
'method' => 'none',
30+
),
31+
)
32+
);
33+
34+
$registry->register(
35+
'test_server_only_service',
36+
array(
37+
'name' => 'Test Server Only Service',
38+
'description' => 'A server-only service with no JS render.',
39+
'type' => 'custom_service',
40+
'authentication' => array(
41+
'method' => 'none',
42+
),
43+
)
44+
);
45+
}
46+
);
47+
48+
// Enqueue the script module on the connectors page.
49+
add_action(
50+
'admin_enqueue_scripts',
51+
static function () {
52+
if ( ! isset( $_GET['page'] ) || 'options-connectors-wp-admin' !== $_GET['page'] ) {
53+
return;
54+
}
55+
56+
wp_register_script_module(
57+
'gutenberg-test-connectors-js-extensibility',
58+
plugins_url( 'connectors-js-extensibility/index.mjs', __FILE__ ),
59+
array(
60+
array(
61+
'id' => '@wordpress/connectors',
62+
'import' => 'static',
63+
),
64+
)
65+
);
66+
wp_enqueue_script_module( 'gutenberg-test-connectors-js-extensibility' );
67+
}
68+
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Script module that demonstrates client-side connector registration.
3+
*
4+
* The server registers test_custom_service with its name and description.
5+
* This module calls registerConnector() with the same slug to add a render
6+
* function. The store merges both registrations, so the final connector
7+
* combines the render function from JS with the metadata from PHP.
8+
*/
9+
10+
// eslint-disable-next-line import/no-extraneous-dependencies
11+
import {
12+
__experimentalRegisterConnector as registerConnector,
13+
__experimentalConnectorItem as ConnectorItem,
14+
} from '@wordpress/connectors';
15+
16+
const h = window.React.createElement;
17+
18+
// Register the render function for the connector.
19+
registerConnector( 'test_custom_service', {
20+
render: ( props ) =>
21+
h(
22+
ConnectorItem,
23+
{
24+
className: 'connector-item--test_custom_service',
25+
name: props.name,
26+
description: props.description,
27+
logo: props.logo,
28+
},
29+
h(
30+
'p',
31+
{ className: 'test-custom-service-content' },
32+
'Custom rendered content for testing.'
33+
)
34+
),
35+
} );

routes/connectors-home/default-connectors.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface ConnectorData {
2828
name: string;
2929
description: string;
3030
logoUrl?: string;
31-
type: 'ai_provider';
31+
type: string;
3232
plugin?: {
3333
slug: string;
3434
isInstalled: boolean;
@@ -231,28 +231,26 @@ function ApiKeyConnector( {
231231
export function registerDefaultConnectors() {
232232
const connectors = getConnectorData();
233233

234-
const sanitize = ( s: string ) => s.replace( /[^a-z0-9-]/gi, '-' );
234+
const sanitize = ( s: string ) => s.replace( /[^a-z0-9-_]/gi, '-' );
235235

236236
for ( const [ connectorId, data ] of Object.entries( connectors ) ) {
237237
const { authentication } = data;
238238

239-
if (
240-
data.type !== 'ai_provider' ||
241-
authentication.method !== 'api_key'
242-
) {
243-
continue;
244-
}
245-
246-
const connectorName = `${ sanitize( data.type ) }/${ sanitize(
247-
connectorId
248-
) }`;
249-
registerConnector( connectorName, {
239+
const connectorName = sanitize( connectorId );
240+
const args: Partial< Omit< ConnectorConfig, 'slug' > > = {
250241
name: data.name,
251242
description: data.description,
252243
logo: getConnectorLogo( connectorId, data.logoUrl ),
253244
authentication,
254245
plugin: data.plugin,
255-
render: ApiKeyConnector,
256-
} );
246+
};
247+
if (
248+
data.type === 'ai_provider' &&
249+
authentication.method === 'api_key'
250+
) {
251+
args.render = ApiKeyConnector;
252+
}
253+
254+
registerConnector( connectorName, args );
257255
}
258256
}

routes/connectors-home/stage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ function ConnectorsPage() {
4242
[]
4343
);
4444

45-
const isEmpty = connectors.length === 0;
45+
const renderableConnectors = connectors.filter(
46+
( connector: ConnectorConfig ) => connector.render
47+
);
48+
const isEmpty = renderableConnectors.length === 0;
4649

4750
return (
4851
<Page

test/e2e/specs/admin/connectors.spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,66 @@ test.describe( 'Connectors', () => {
518518
} );
519519
} );
520520
} );
521+
522+
test.describe( 'JS extensibility', () => {
523+
const PLUGIN_SLUG = 'gutenberg-test-connectors-js-extensibility';
524+
525+
test.beforeAll( async ( { requestUtils } ) => {
526+
await requestUtils.activatePlugin( PLUGIN_SLUG );
527+
} );
528+
529+
test.afterAll( async ( { requestUtils } ) => {
530+
await requestUtils.deactivatePlugin( PLUGIN_SLUG );
531+
} );
532+
533+
test( 'should not display a card for a server-only connector without a JS render function', async ( {
534+
page,
535+
admin,
536+
} ) => {
537+
await admin.visitAdminPage(
538+
SETTINGS_PAGE_PATH,
539+
CONNECTORS_PAGE_QUERY
540+
);
541+
542+
// The server registers test_server_only_service but no JS
543+
// registerConnector call provides a render function for it,
544+
// so no card should appear in the UI.
545+
await expect(
546+
page.getByRole( 'heading', {
547+
name: 'Test Server Only Service',
548+
level: 2,
549+
} )
550+
).toBeHidden();
551+
} );
552+
553+
test( 'should display a custom connector registered via JS with merging strategy', async ( {
554+
page,
555+
admin,
556+
} ) => {
557+
await admin.visitAdminPage(
558+
SETTINGS_PAGE_PATH,
559+
CONNECTORS_PAGE_QUERY
560+
);
561+
562+
const card = page.locator( '.connector-item--test_custom_service' );
563+
await expect( card ).toBeVisible();
564+
565+
// Verify the custom content from the render function is visible.
566+
await expect(
567+
card.getByText( 'Custom rendered content for testing.' )
568+
).toBeVisible();
569+
570+
// Verify label and description from the server-side PHP registration
571+
// are merged with the client-side JS render function.
572+
await expect(
573+
card.getByRole( 'heading', {
574+
name: 'Test Custom Service',
575+
level: 2,
576+
} )
577+
).toBeVisible();
578+
await expect(
579+
card.getByText( 'A custom service for E2E testing.' )
580+
).toBeVisible();
581+
} );
582+
} );
521583
} );

0 commit comments

Comments
 (0)