diff --git a/lib/compat/wordpress-7.0/php-only-blocks.php b/lib/compat/wordpress-7.0/php-only-blocks.php
index 47b073f53fc095..43e862be3814bc 100644
--- a/lib/compat/wordpress-7.0/php-only-blocks.php
+++ b/lib/compat/wordpress-7.0/php-only-blocks.php
@@ -34,3 +34,49 @@ function gutenberg_register_auto_register_blocks() {
}
add_action( 'enqueue_block_editor_assets', 'gutenberg_register_auto_register_blocks', 5 );
+
+/**
+ * Mark user-defined attributes for auto-generated inspector controls.
+ *
+ * This filter runs during block type registration, before the WP_Block_Type
+ * is instantiated. Block supports add their attributes AFTER the block type
+ * is created (via WP_Block_Supports::register_attributes()), so any attributes
+ * present at this stage are user-defined.
+ *
+ * The marker tells generateFieldsFromAttributes() which attributes should
+ * get auto-generated inspector controls. Attributes are excluded if they:
+ * - Have a 'source' (HTML-derived, edited inline not via inspector)
+ * - Have role 'local' (internal state, not user-configurable)
+ * - Were added by block supports (added after this filter runs)
+ *
+ * @param array $settings Array of block type arguments for registration.
+ * @return array Modified settings with marked attributes.
+ */
+function gutenberg_mark_auto_inspector_control_attributes( $settings ) {
+ if ( empty( $settings['attributes'] ) || ! is_array( $settings['attributes'] ) ) {
+ return $settings;
+ }
+
+ // Only process blocks with auto_register flag.
+ $has_auto_register = ! empty( $settings['supports']['auto_register'] );
+ if ( ! $has_auto_register ) {
+ return $settings;
+ }
+
+ foreach ( $settings['attributes'] as $name => $def ) {
+ // Skip HTML-derived attributes (edited inline, not via inspector).
+ if ( ! empty( $def['source'] ) ) {
+ continue;
+ }
+ // Skip internal attributes (not user-configurable).
+ if ( isset( $def['role'] ) && 'local' === $def['role'] ) {
+ continue;
+ }
+ $settings['attributes'][ $name ]['__experimentalAutoInspectorControl'] = true;
+ }
+
+ return $settings;
+}
+
+// Priority 5 to mark original attributes before other filters (priority 10+) might add their own.
+add_filter( 'register_block_type_args', 'gutenberg_mark_auto_inspector_control_attributes', 5 );
diff --git a/packages/block-editor/src/hooks/auto-inspector-controls.js b/packages/block-editor/src/hooks/auto-inspector-controls.js
new file mode 100644
index 00000000000000..cf4e00156326b0
--- /dev/null
+++ b/packages/block-editor/src/hooks/auto-inspector-controls.js
@@ -0,0 +1,95 @@
+/**
+ * WordPress dependencies
+ */
+import { getBlockType } from '@wordpress/blocks';
+import { PanelBody } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { DataForm } from '@wordpress/dataviews';
+import { useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import InspectorControls from '../components/inspector-controls';
+import { useBlockEditingMode } from '../components/block-editing-mode';
+import { store as blockEditorStore } from '../store';
+import { generateFieldsFromAttributes } from './generate-fields-from-attributes';
+
+/**
+ * Checks if a block has any attributes marked for auto-generated inspector controls.
+ *
+ * @param {Object} blockTypeAttributes - The block type's attributes object.
+ * @return {boolean} True if any attribute has __experimentalAutoInspectorControl marker.
+ */
+function hasAutoInspectorControlAttributes( blockTypeAttributes ) {
+ if ( ! blockTypeAttributes ) {
+ return false;
+ }
+ return Object.values( blockTypeAttributes ).some(
+ ( attr ) => attr?.__experimentalAutoInspectorControl
+ );
+}
+
+/**
+ * Renders DataForm-based inspector controls for auto-registered PHP-only blocks.
+ *
+ * Fields are generated on-the-fly from attributes marked with `__experimentalAutoInspectorControl`
+ * during PHP registration.
+ *
+ * @param {Object} props Component props.
+ * @param {string} props.name Block name.
+ * @param {string} props.clientId Block client ID.
+ * @param {Function} props.setAttributes Function to update block attributes.
+ */
+function AutoRegisterControls( { name, clientId, setAttributes } ) {
+ const blockEditingMode = useBlockEditingMode();
+
+ const attributes = useSelect(
+ ( select ) => select( blockEditorStore ).getBlockAttributes( clientId ),
+ [ clientId ]
+ );
+
+ const blockType = getBlockType( name );
+
+ // Generate fields from user-defined attributes marked by PHP.
+ // The __experimentalAutoInspectorControl marker excludes block support attributes
+ // (which have their own UI) and internal state (role: 'local').
+ // Memoized since blockType.attributes don't change after registration.
+ const { fields, form } = useMemo( () => {
+ if ( ! blockType?.attributes ) {
+ return { fields: [], form: { fields: [] } };
+ }
+ return generateFieldsFromAttributes( blockType.attributes );
+ }, [ blockType?.attributes ] );
+
+ if ( blockEditingMode !== 'default' ) {
+ return null;
+ }
+
+ if ( ! fields || fields.length === 0 ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default {
+ edit: AutoRegisterControls,
+ attributeKeys: [],
+ hasSupport( name ) {
+ const blockType = getBlockType( name );
+ return hasAutoInspectorControlAttributes( blockType?.attributes );
+ },
+};
diff --git a/packages/block-editor/src/hooks/generate-fields-from-attributes.js b/packages/block-editor/src/hooks/generate-fields-from-attributes.js
new file mode 100644
index 00000000000000..30d892a9509168
--- /dev/null
+++ b/packages/block-editor/src/hooks/generate-fields-from-attributes.js
@@ -0,0 +1,65 @@
+/**
+ * Generates DataForm field definitions from block attributes.
+ *
+ * This utility enables PHP-only blocks to have auto-generated inspector controls
+ * by converting block attribute definitions into DataForm field definitions.
+ *
+ * @param {Object} attributes - Block type attributes from block registration
+ * @return {{ fields: Array, form: Object }} fieldsKey and formKey values
+ */
+export function generateFieldsFromAttributes( attributes ) {
+ const fields = [];
+ const fieldIds = [];
+
+ Object.entries( attributes ).forEach( ( [ name, def ] ) => {
+ if ( ! def.__experimentalAutoInspectorControl ) {
+ return;
+ }
+
+ const field = createFieldFromAttribute( name, def );
+ if ( field ) {
+ fields.push( field );
+ fieldIds.push( name );
+ }
+ } );
+
+ return {
+ fields,
+ form: { fields: fieldIds },
+ };
+}
+
+/**
+ * Creates a DataForm field definition from a block attribute definition.
+ *
+ * @param {string} name - The attribute name
+ * @param {Object} def - The attribute definition from block.json
+ * @return {Object|null} DataForm field definition or null if type not supported
+ */
+function createFieldFromAttribute( name, def ) {
+ const type = def.type;
+
+ // Skip unsupported types (object, union types, etc.)
+ // Supported: stringβtext, number, integer, boolean (1:1 with DataForm)
+ if ( ! [ 'string', 'number', 'integer', 'boolean' ].includes( type ) ) {
+ return null;
+ }
+
+ const field = {
+ id: name,
+ label: def.label || name,
+ // Only 'string' needs mapping to 'text'; others are 1:1 with DataForm types.
+ // This mapping will be unnecessary once #74105 lands.
+ type: type === 'string' ? 'text' : type,
+ };
+
+ // Add elements for enums (DataForm shows select UI when elements are present)
+ if ( def.enum && Array.isArray( def.enum ) ) {
+ field.elements = def.enum.map( ( value ) => ( {
+ value,
+ label: String( value ),
+ } ) );
+ }
+
+ return field;
+}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 4234537efe6642..764d4d81af5720 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -36,6 +36,7 @@ import blockBindingsPanel from './block-bindings';
import listView from './list-view';
import './block-renaming';
import './grid-visualizer';
+import autoInspectorControls from './auto-inspector-controls';
createBlockEditFilter(
[
@@ -54,6 +55,7 @@ createBlockEditFilter(
childLayout,
allowedBlocks,
listView,
+ autoInspectorControls,
].filter( Boolean )
);
createBlockListBlockFilter( [
diff --git a/packages/block-editor/src/hooks/test/auto-inspector-controls.js b/packages/block-editor/src/hooks/test/auto-inspector-controls.js
new file mode 100644
index 00000000000000..34b19ec936a9d8
--- /dev/null
+++ b/packages/block-editor/src/hooks/test/auto-inspector-controls.js
@@ -0,0 +1,115 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ registerBlockType,
+ unregisterBlockType,
+ getBlockType,
+} from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import autoInspectorControls from '../auto-inspector-controls';
+
+describe( 'auto-inspector-controls', () => {
+ const blockName = 'test/auto-inspector-controls-block';
+
+ afterEach( () => {
+ if ( getBlockType( blockName ) ) {
+ unregisterBlockType( blockName );
+ }
+ } );
+
+ describe( 'hasSupport()', () => {
+ it( 'should return false for blocks without __experimentalAutoInspectorControl markers', () => {
+ registerBlockType( blockName, {
+ title: 'Test Block',
+ category: 'text',
+ attributes: {
+ content: {
+ type: 'string',
+ },
+ },
+ edit: () => null,
+ save: () => null,
+ } );
+
+ expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
+ false
+ );
+ } );
+
+ it( 'should return true for blocks with __experimentalAutoInspectorControl markers', () => {
+ registerBlockType( blockName, {
+ title: 'Test Block',
+ category: 'text',
+ attributes: {
+ title: {
+ type: 'string',
+ __experimentalAutoInspectorControl: true,
+ },
+ count: {
+ type: 'integer',
+ __experimentalAutoInspectorControl: true,
+ },
+ },
+ edit: () => null,
+ save: () => null,
+ } );
+
+ expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
+ true
+ );
+ } );
+
+ it( 'should return false for unregistered blocks', () => {
+ expect(
+ autoInspectorControls.hasSupport( 'non/existent-block' )
+ ).toBe( false );
+ } );
+
+ it( 'should return false for blocks with no attributes', () => {
+ registerBlockType( blockName, {
+ title: 'Test Block',
+ category: 'text',
+ edit: () => null,
+ save: () => null,
+ } );
+
+ expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
+ false
+ );
+ } );
+
+ it( 'should return true when at least one attribute has __experimentalAutoInspectorControl', () => {
+ registerBlockType( blockName, {
+ title: 'Test Block',
+ category: 'text',
+ attributes: {
+ // This one has the marker
+ title: {
+ type: 'string',
+ __experimentalAutoInspectorControl: true,
+ },
+ // This one doesn't (e.g., added by block supports)
+ className: {
+ type: 'string',
+ },
+ },
+ edit: () => null,
+ save: () => null,
+ } );
+
+ expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
+ true
+ );
+ } );
+ } );
+
+ describe( 'attributeKeys', () => {
+ it( 'should be an empty array', () => {
+ expect( autoInspectorControls.attributeKeys ).toEqual( [] );
+ } );
+ } );
+} );
diff --git a/packages/block-editor/src/hooks/test/generate-fields-from-attributes.js b/packages/block-editor/src/hooks/test/generate-fields-from-attributes.js
new file mode 100644
index 00000000000000..7867272dfe2b40
--- /dev/null
+++ b/packages/block-editor/src/hooks/test/generate-fields-from-attributes.js
@@ -0,0 +1,216 @@
+/**
+ * Internal dependencies
+ */
+import { generateFieldsFromAttributes } from '../generate-fields-from-attributes';
+
+/**
+ * Helper to mark attributes for auto-generated inspector controls.
+ * In production, this marker is added by PHP during block registration.
+ *
+ * @param {Object} attrs - Attributes object
+ * @return {Object} Attributes with __experimentalAutoInspectorControl marker
+ */
+function markForAutoInspectorControl( attrs ) {
+ const result = {};
+ for ( const [ name, def ] of Object.entries( attrs ) ) {
+ result[ name ] = { ...def, __experimentalAutoInspectorControl: true };
+ }
+ return result;
+}
+
+describe( 'generateFieldsFromAttributes', () => {
+ it( 'should generate text field for string attribute', () => {
+ const attributes = markForAutoInspectorControl( {
+ message: {
+ type: 'string',
+ default: 'Hello',
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ] ).toEqual( {
+ id: 'message',
+ label: 'message',
+ type: 'text',
+ } );
+ expect( result.form.fields ).toContain( 'message' );
+ } );
+
+ it( 'should generate number field for number attribute', () => {
+ const attributes = markForAutoInspectorControl( {
+ amount: {
+ type: 'number',
+ default: 10,
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ] ).toEqual( {
+ id: 'amount',
+ label: 'amount',
+ type: 'number',
+ } );
+ } );
+
+ it( 'should generate integer field for integer attribute', () => {
+ const attributes = markForAutoInspectorControl( {
+ count: {
+ type: 'integer',
+ default: 5,
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ] ).toEqual( {
+ id: 'count',
+ label: 'count',
+ type: 'integer',
+ } );
+ } );
+
+ it( 'should generate boolean field for boolean attribute', () => {
+ const attributes = markForAutoInspectorControl( {
+ enabled: {
+ type: 'boolean',
+ default: true,
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ] ).toEqual( {
+ id: 'enabled',
+ label: 'enabled',
+ type: 'boolean',
+ } );
+ } );
+
+ it( 'should generate text field with elements for enum attribute', () => {
+ const attributes = markForAutoInspectorControl( {
+ size: {
+ type: 'string',
+ enum: [ 'small', 'medium', 'large' ],
+ default: 'medium',
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ // DataForm automatically uses a select control when elements are present
+ expect( result.fields[ 0 ] ).toEqual( {
+ id: 'size',
+ label: 'size',
+ type: 'text',
+ elements: [
+ { value: 'small', label: 'small' },
+ { value: 'medium', label: 'medium' },
+ { value: 'large', label: 'large' },
+ ],
+ } );
+ } );
+
+ it( 'should skip unsupported attribute types', () => {
+ const attributes = markForAutoInspectorControl( {
+ message: {
+ type: 'string',
+ default: 'Hello',
+ },
+ items: {
+ type: 'array',
+ default: [],
+ },
+ config: {
+ type: 'object',
+ default: {},
+ },
+ unionType: {
+ type: [ 'string', 'null' ],
+ default: null,
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ // Only string attribute should generate a field
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ].id ).toBe( 'message' );
+ } );
+
+ it( 'should return empty fields array for empty attributes', () => {
+ const result = generateFieldsFromAttributes( {} );
+
+ expect( result.fields ).toHaveLength( 0 );
+ expect( result.form.fields ).toHaveLength( 0 );
+ } );
+
+ it( 'should use custom label when provided', () => {
+ const attributes = markForAutoInspectorControl( {
+ bgColor: {
+ type: 'string',
+ label: 'Background Color',
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields[ 0 ].label ).toBe( 'Background Color' );
+ } );
+
+ it( 'should skip attributes without __experimentalAutoInspectorControl marker', () => {
+ const attributes = {
+ userDefined: {
+ type: 'string',
+ __experimentalAutoInspectorControl: true,
+ },
+ supportAdded: {
+ type: 'string',
+ // No marker = simulate attribute added by block supports
+ },
+ };
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 1 );
+ expect( result.fields[ 0 ].id ).toBe( 'userDefined' );
+ expect( result.form.fields ).not.toContain( 'supportAdded' );
+ } );
+
+ it( 'should generate multiple fields for multiple attributes', () => {
+ const attributes = markForAutoInspectorControl( {
+ title: {
+ type: 'string',
+ default: '',
+ },
+ count: {
+ type: 'integer',
+ default: 0,
+ },
+ enabled: {
+ type: 'boolean',
+ default: false,
+ },
+ size: {
+ type: 'string',
+ enum: [ 'small', 'large' ],
+ },
+ } );
+
+ const result = generateFieldsFromAttributes( attributes );
+
+ expect( result.fields ).toHaveLength( 4 );
+ expect( result.form.fields ).toEqual( [
+ 'title',
+ 'count',
+ 'enabled',
+ 'size',
+ ] );
+ } );
+} );
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 718e83a0bf8e8d..01376a4f129d8c 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -350,6 +350,7 @@ export const registerCoreBlocks = (
...( ( bootstrappedBlockType?.apiVersion ?? 0 ) < 3 && {
apiVersion: 3,
} ),
+ // Inspector controls are rendered by the auto-register hook in block-editor
edit: function Edit( { attributes } ) {
const blockProps = useBlockProps();
const { content, status, error } = useServerSideRender( {
diff --git a/packages/e2e-tests/plugins/server-side-rendered-block.php b/packages/e2e-tests/plugins/server-side-rendered-block.php
index 7fcc6d9372cbf9..db00bda4c7439e 100644
--- a/packages/e2e-tests/plugins/server-side-rendered-block.php
+++ b/packages/e2e-tests/plugins/server-side-rendered-block.php
@@ -86,5 +86,85 @@ static function () {
},
)
);
+
+ // PHP-only block with auto-generated controls from various attribute types
+ register_block_type(
+ 'test/auto-register-with-controls',
+ array(
+ 'title' => 'Auto Register With Controls',
+ 'icon' => 'admin-generic',
+ 'category' => 'widgets',
+ 'description' => 'A test block for auto-generated inspector controls',
+ 'keywords' => array( 'autoregister', 'controls', 'dataform' ),
+ // Labels are translatable via __() in real plugins.
+ 'attributes' => array(
+ 'title' => array(
+ 'type' => 'string',
+ 'default' => 'My Emoji Collection',
+ 'label' => 'Title',
+ ),
+ 'count' => array(
+ 'type' => 'integer',
+ 'default' => 5,
+ 'label' => 'Count',
+ ),
+ 'spacing' => array(
+ 'type' => 'number',
+ 'default' => 0.1,
+ 'label' => 'Spacing',
+ ),
+ 'showEmojis' => array(
+ 'type' => 'boolean',
+ 'default' => true,
+ 'label' => 'Show Emojis',
+ ),
+ 'emoji' => array(
+ 'type' => 'string',
+ 'enum' => array( 'β', 'β€οΈ', 'π', 'π', 'π' ),
+ 'default' => 'β',
+ 'label' => 'Emoji',
+ ),
+ // Should NOT get a control (has source - HTML-derived)
+ 'content' => array(
+ 'type' => 'string',
+ 'source' => 'html',
+ ),
+ // Should NOT get a control (role: local - internal state)
+ 'internalState' => array(
+ 'type' => 'string',
+ 'role' => 'local',
+ 'default' => 'internal',
+ ),
+ ),
+ 'render_callback' => static function ( $attributes ) {
+ $wrapper_attributes = get_block_wrapper_attributes(
+ array(
+ 'style' => 'padding: 20px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px;',
+ )
+ );
+ $title = esc_html( $attributes['title'] );
+ $count = min( 20, max( 0, absint( $attributes['count'] ) ) );
+ $spacing = floatval( $attributes['spacing'] );
+ $show_emojis = $attributes['showEmojis'];
+ $emoji = $attributes['emoji'];
+
+ $emoji_display = $show_emojis ? str_repeat( $emoji . ' ', $count ) : 'Emojis hidden';
+
+ return sprintf(
+ '
',
+ $wrapper_attributes,
+ $title,
+ $spacing,
+ $emoji_display
+ );
+ },
+ 'supports' => array(
+ 'auto_register' => true,
+ ),
+ )
+ );
}
);
diff --git a/phpunit/blocks/auto-register-blocks-test.php b/phpunit/blocks/auto-register-blocks-test.php
new file mode 100644
index 00000000000000..a1722c50581a13
--- /dev/null
+++ b/phpunit/blocks/auto-register-blocks-test.php
@@ -0,0 +1,106 @@
+ array( 'auto_register' => true ),
+ 'attributes' => array(
+ 'title' => array( 'type' => 'string' ),
+ 'count' => array( 'type' => 'integer' ),
+ ),
+ );
+
+ $result = gutenberg_mark_auto_inspector_control_attributes( $settings );
+
+ $this->assertTrue( $result['attributes']['title']['__experimentalAutoInspectorControl'] );
+ $this->assertTrue( $result['attributes']['count']['__experimentalAutoInspectorControl'] );
+ }
+
+ /**
+ * Tests that attributes are not marked without auto_register flag.
+ */
+ public function test_does_not_mark_attributes_without_auto_register() {
+ $settings = array(
+ 'attributes' => array(
+ 'title' => array( 'type' => 'string' ),
+ ),
+ );
+
+ $result = gutenberg_mark_auto_inspector_control_attributes( $settings );
+
+ $this->assertArrayNotHasKey( '__experimentalAutoInspectorControl', $result['attributes']['title'] );
+ }
+
+ /**
+ * Tests that attributes with source are excluded.
+ */
+ public function test_excludes_attributes_with_source() {
+ $settings = array(
+ 'supports' => array( 'auto_register' => true ),
+ 'attributes' => array(
+ 'title' => array( 'type' => 'string' ),
+ 'content' => array(
+ 'type' => 'string',
+ 'source' => 'html',
+ ),
+ ),
+ );
+
+ $result = gutenberg_mark_auto_inspector_control_attributes( $settings );
+
+ $this->assertTrue( $result['attributes']['title']['__experimentalAutoInspectorControl'] );
+ $this->assertArrayNotHasKey( '__experimentalAutoInspectorControl', $result['attributes']['content'] );
+ }
+
+ /**
+ * Tests that attributes with role: local are excluded.
+ *
+ * Example: The 'blob' attribute in media blocks (image, video, file, audio)
+ * stores a temporary blob URL during file upload. This is internal state
+ * that shouldn't be shown in the inspector or saved to the database.
+ */
+ public function test_excludes_attributes_with_role_local() {
+ $settings = array(
+ 'supports' => array( 'auto_register' => true ),
+ 'attributes' => array(
+ 'title' => array( 'type' => 'string' ),
+ 'blob' => array(
+ 'type' => 'string',
+ 'role' => 'local',
+ ),
+ ),
+ );
+
+ $result = gutenberg_mark_auto_inspector_control_attributes( $settings );
+
+ $this->assertTrue( $result['attributes']['title']['__experimentalAutoInspectorControl'] );
+ $this->assertArrayNotHasKey( '__experimentalAutoInspectorControl', $result['attributes']['blob'] );
+ }
+
+ /**
+ * Tests that empty attributes are handled gracefully.
+ */
+ public function test_handles_empty_attributes() {
+ $settings = array(
+ 'supports' => array( 'auto_register' => true ),
+ );
+
+ $result = gutenberg_mark_auto_inspector_control_attributes( $settings );
+
+ $this->assertSame( $settings, $result );
+ }
+}
diff --git a/test/e2e/specs/editor/plugins/server-side-rendered-block.spec.js b/test/e2e/specs/editor/plugins/server-side-rendered-block.spec.js
index 148f57b0a0e556..7050614a270ada 100644
--- a/test/e2e/specs/editor/plugins/server-side-rendered-block.spec.js
+++ b/test/e2e/specs/editor/plugins/server-side-rendered-block.spec.js
@@ -232,4 +232,35 @@ test.describe( 'PHP-only auto-register blocks', () => {
);
await expect( colorText ).toBeVisible();
} );
+
+ test( 'should generate inspector controls from block attributes', async ( {
+ editor,
+ page,
+ } ) => {
+ // Insert the block with auto-generated controls
+ await editor.insertBlock( {
+ name: 'test/auto-register-with-controls',
+ } );
+
+ // Open the document settings sidebar
+ await editor.openDocumentSettingsSidebar();
+
+ // Verify auto-generated controls are present
+ // String attribute β text input
+ await expect( page.getByLabel( 'Title' ) ).toBeVisible();
+
+ // Integer attribute β number input
+ await expect( page.getByLabel( 'Count' ) ).toBeVisible();
+
+ // Number attribute β number control
+ await expect( page.getByLabel( 'Spacing' ) ).toBeVisible();
+
+ // Boolean attribute β toggle/checkbox
+ await expect( page.getByLabel( 'Show Emojis' ) ).toBeVisible();
+
+ // Enum attribute β select control
+ await expect(
+ page.getByLabel( 'Emoji', { exact: true } )
+ ).toBeVisible();
+ } );
} );