diff --git a/lib/experimental/class-wp-theme-json-gutenberg.php b/lib/experimental/class-wp-theme-json-gutenberg.php index 53809cb692aa65..db3f479228e838 100644 --- a/lib/experimental/class-wp-theme-json-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-gutenberg.php @@ -28,4 +28,210 @@ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_6_1 { * @var array */ protected static $blocks_metadata = null; + + /** + * Sanitizes the input according to the schemas. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * + * @param array $input Structure to sanitize. + * @param array $valid_block_names List of valid block names. + * @param array $valid_element_names List of valid element names. + * @return array The sanitized output. + */ + protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { + + $output = array(); + + if ( ! is_array( $input ) ) { + return $output; + } + + // Preserve only the top most level keys. + $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); + + // Remove any rules that are annotated as "top" in VALID_STYLES constant. + // Some styles are only meant to be available at the top-level (e.g.: blockGap), + // hence, the schema for blocks & elements should not have them. + $styles_non_top_level = static::VALID_STYLES; + foreach ( array_keys( $styles_non_top_level ) as $section ) { + if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { + foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { + if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { + unset( $styles_non_top_level[ $section ][ $prop ] ); + } + } + } + } + + // Build the schema based on valid block & element names. + $schema = array(); + $schema_styles_elements = array(); + + // Set allowed element pseudo selectors based on per element allow list. + // Target data structure in schema: + // e.g. + // - top level elements: `$schema['styles']['elements']['link'][':hover']`. + // - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + foreach ( $valid_element_names as $element ) { + $schema_styles_elements[ $element ] = $styles_non_top_level; + + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + + $schema_styles_blocks = array(); + foreach ( $valid_block_names as $block ) { + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + } + + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + + // Remove anything that's not present in the schema. + foreach ( array( 'styles', 'settings' ) as $subtree ) { + if ( ! isset( $input[ $subtree ] ) ) { + continue; + } + + if ( ! is_array( $input[ $subtree ] ) ) { + unset( $output[ $subtree ] ); + continue; + } + + // First clean up everything, save for the blocks. + $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); + + // Then, clean up blocks and append that to the result. + if ( 'settings' === $subtree && isset( $input[ $subtree ]['blocks'] ) ) { + $result['blocks'] = static::sanitize_blocks( $input[ $subtree ]['blocks'], $valid_block_names, $schema[ $subtree ] ); + } + + if ( empty( $result ) ) { + unset( $output[ $subtree ] ); + } else { + $output[ $subtree ] = $result; + } + } + + return $output; + } + + /** + * Sanitize the blocks section that can be found in settings and styles, including nested blocks. + * + * @param array $current_block The current block to break down. + * @param array $valid_block_names List of valid block names. + * @param array $schema Valid schema that is allowed for this block. + * @param array $result The sanitized version of the block. + * @return array The sanitized blocks output + */ + protected static function sanitize_blocks( $current_block, $valid_block_names, $schema, $result = array() ) { + foreach ( $current_block as $block_name => $block ) { + if ( in_array( $block_name, $valid_block_names, true ) ) { + $base_result = static::remove_keys_not_in_schema( $block, $schema ); + $sub_result = static::sanitize_blocks( $block, $valid_block_names, $schema ); + + $result[ $block_name ] = array_merge( $base_result, $sub_result ); + } + } + + return $result; + } + + /** + * Builds metadata for the setting nodes, which returns in the form of: + * + * [ + * [ + * 'path' => ['path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node' + * ], + * [ + * 'path' => [ 'path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node' + * ], + * ] + * + * @since 5.8.0 + * + * @param array $theme_json The tree to extract setting nodes from. + * @param array $selectors List of selectors per block. + * @return array + */ + protected static function get_setting_nodes( $theme_json, $selectors = array() ) { + $nodes = array(); + if ( ! isset( $theme_json['settings'] ) ) { + return $nodes; + } + + // Top-level. + $nodes[] = array( + 'path' => array( 'settings' ), + 'selector' => static::ROOT_BLOCK_SELECTOR, + ); + + // Calculate paths for blocks. + if ( ! isset( $theme_json['settings']['blocks'] ) ) { + return $nodes; + } + + $valid_block_names = array_keys( static::get_blocks_metadata() ); + + return static::get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $theme_json['settings']['blocks'] ); + } + + /** + * Builds the metadata for settings.blocks, whilst ensuring support for nested blocks. This returns in the form of: + * + * [ + * [ + * 'path' => ['path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node' + * ], + * [ + * 'path' => [ 'path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node' + * ], + * ] + * + * @param array $selectors List of selectors per block. + * @param array $valid_block_names List of valid block names. + * @param array $nodes The metadata of the nodes that have been built so far. + * @param array $current_block The current block to break down. + * @param array $current_selector The current selector of the current block. + * @param array $current_path The current path to the block. + * @return array + */ + protected static function get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $current_block, $current_selector = null, $current_path = array() ) { + foreach ( $current_block as $block_name => $block ) { + if ( in_array( $block_name, $valid_block_names, true ) ) { + + $selector = is_null( $current_selector ) ? null : $current_selector; + if ( isset( $selectors[ $block_name ]['selector'] ) ) { + $selector = $selector . ' ' . $selectors[ $block_name ]['selector']; + } + + $path = empty( $current_path ) ? array( 'settings', 'blocks' ) : $current_path; + array_push( $path, $block_name ); + + $nodes[] = array( + 'path' => $path, + 'selector' => $selector, + ); + + $nodes = static::get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $block, $selector, $path ); + } + } + + return $nodes; + } + } diff --git a/packages/block-editor/src/components/use-setting/index.js b/packages/block-editor/src/components/use-setting/index.js index 3d37a85de16934..e979118657bbba 100644 --- a/packages/block-editor/src/components/use-setting/index.js +++ b/packages/block-editor/src/components/use-setting/index.js @@ -93,6 +93,69 @@ const removeCustomPrefixes = ( path ) => { return prefixedFlags[ path ] || path; }; +/** + * Find block settings nested in other block settings. + * + * Given an array of blocks names from the top level of the editor to the + * current block (`blockNamePath`), return the value for the deepest-nested + * settings value that applies to the current block. + * + * If two setting values share the same nesting depth, use the last one that + * occurs in settings (like CSS). + * + * @param {string[]} blockNamePath Block names representing the path to the + * current block from the top level of the + * block editor. + * @param {string} normalizedPath Path to the setting being retrieved. + * @param {Object} settings Object containing all block settings. + * @param {Object} result Optional. Object with keys `depth` and + * `value` used to track current most-nested + * setting. + * @param {number} depth Optional. The current recursion depth used + * to calculate the most-nested setting. + * @return {Object} Object with keys `depth` and `value`. + * Destructure the `value` key for the result. + */ +const getNestedSetting = ( + blockNamePath, + normalizedPath, + settings, + result = { depth: 0, value: undefined }, + depth = 1 +) => { + const [ currentBlockName, ...remainingBlockNames ] = blockNamePath; + const blockSettings = settings[ currentBlockName ]; + + if ( remainingBlockNames.length === 0 ) { + const settingValue = get( blockSettings, normalizedPath ); + + if ( settingValue !== undefined && depth >= result.depth ) { + result.depth = depth; + result.value = settingValue; + } + + return result; + } else if ( blockSettings !== undefined ) { + // Recurse into the parent block's settings + result = getNestedSetting( + remainingBlockNames, + normalizedPath, + blockSettings, + result, + depth + 1 + ); + } + + // Continue down the array of blocks + return getNestedSetting( + remainingBlockNames, + normalizedPath, + settings, + result, + depth + ); +}; + /** * Hook that retrieves the given setting for the block instance in use. * @@ -121,10 +184,12 @@ export default function useSetting( path ) { let result; const normalizedPath = removeCustomPrefixes( path ); + const blockParentIds = + select( blockEditorStore ).getBlockParents( clientId ); // 1. Take settings from the block instance or its ancestors. const candidates = [ - ...select( blockEditorStore ).getBlockParents( clientId ), + ...blockParentIds, clientId, // The current block is added last, so it overwrites any ancestor. ]; candidates.forEach( ( candidateClientId ) => { @@ -157,11 +222,33 @@ export default function useSetting( path ) { // 2. Fall back to the settings from the block editor store (__experimentalFeatures). const settings = select( blockEditorStore ).getSettings(); + + // 2.1 Check for block-specific settings from block editor store + if ( result === undefined && blockName !== '' ) { + const blockNamePath = [ + ...blockParentIds.map( ( parentId ) => + select( blockEditorStore ).getBlockName( parentId ) + ), + blockName, + ]; + + const blockSettings = get( + settings, + '__experimentalFeatures.blocks', + {} + ); + + ( { value: result } = getNestedSetting( + blockNamePath, + normalizedPath, + blockSettings + ) ); + } + + // 2.2 Default to top-level settings from the block editor store if ( result === undefined ) { const defaultsPath = `__experimentalFeatures.${ normalizedPath }`; - const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ normalizedPath }`; - result = - get( settings, blockPath ) ?? get( settings, defaultsPath ); + result = get( settings, defaultsPath ); } // Return if the setting was found in either the block instance or the store. diff --git a/packages/block-editor/src/components/use-setting/test/index.js b/packages/block-editor/src/components/use-setting/test/index.js new file mode 100644 index 00000000000000..cab00a9a851dcc --- /dev/null +++ b/packages/block-editor/src/components/use-setting/test/index.js @@ -0,0 +1,466 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import useSetting from '..'; +import * as BlockEditContext from '../../block-edit/context'; + +// useSelect() mock functions for blockEditorStore +jest.mock( '@wordpress/data/src/components/use-select' ); + +let selectMock = {}; + +useSelect.mockImplementation( ( callback ) => callback( () => selectMock ) ); + +const mockSettings = ( settings ) => { + selectMock.getSettings = () => ( { + __experimentalFeatures: settings, + } ); +}; + +let blockParentMap = {}; + +const mockBlockParent = ( + childClientId, + { clientId: parentClientId, name: parentName } +) => { + mockBlockName( parentClientId, parentName ); + + blockParentMap[ childClientId ] = parentClientId; + + selectMock.getBlockParents = ( clientId, ascending = false ) => { + const parents = []; + let current = clientId; + + while ( !! blockParentMap[ current ] ) { + current = blockParentMap[ current ]; + parents.push( current ); + } + + return ascending ? parents : parents.reverse(); + }; +}; + +const mockBlockName = ( blockClientId, blockName ) => { + const previousGetBlockName = selectMock.getBlockName; + + selectMock.getBlockName = ( clientId ) => { + if ( clientId === blockClientId ) { + return blockName; + } + + return previousGetBlockName( clientId ); + }; +}; + +const mockCurrentBlockContext = ( + blockContext = { name: '', isSelected: false } +) => { + if ( blockContext.name !== '' && blockContext.clientID !== undefined ) { + mockBlockName( blockContext.clientID, blockContext.name ); + } + + jest.spyOn( BlockEditContext, 'useBlockEditContext' ).mockReturnValue( + blockContext + ); +}; + +describe( 'useSetting', () => { + describe( 'nesting', () => { + beforeEach( () => { + selectMock = { + getSettings: () => ( {} ), + getBlockParents: () => [], + getBlockName: () => '', + }; + + mockCurrentBlockContext( {} ); + + blockParentMap = {}; + } ); + + it( 'uses theme setting', () => { + mockSettings( { + color: { + text: false, + }, + } ); + + expect( useSetting( 'color.text' ) ).toBe( false ); + } ); + + it( 'uses block-specific setting', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/test-block': { + color: { + text: true, + }, + }, + }, + } ); + + mockCurrentBlockContext( { + name: 'core/test-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'does not use block-specific setting from another block', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/test-block': { + color: { + text: true, + }, + }, + }, + } ); + + mockCurrentBlockContext( { + name: 'core/unrelated-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( false ); + } ); + + it( 'uses 2-layer deep nested block setting', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/parent-block': { + color: { + text: false, + }, + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/parent-block { + // core/child-block <- current block + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/parent-block', + clientId: 'client-id-parent-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses 3-layer deep nested block setting', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/grandparent-block': { + 'core/parent-block': { + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/grandparent-block { + // core/parent-block { + // core/child-block <- current block + // } + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/parent-block', + clientId: 'client-id-parent-block', + } ); + + mockBlockParent( 'client-id-parent-block', { + name: 'core/grandparent-block', + clientId: 'client-id-grandparent-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses grandparent 2-layer deep nested block setting', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/grandparent-block': { + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/grandparent-block { + // core/parent-block { + // core/child-block <- current block + // } + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/parent-block', + clientId: 'client-id-parent-block', + } ); + + mockBlockParent( 'client-id-parent-block', { + name: 'core/grandparent-block', + clientId: 'client-id-grandparent-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses more specific nested block setting', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/child-block': { + color: { + text: false, + }, + }, + 'core/parent-block': { + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/parent-block { + // core/child-block <- current block + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/parent-block', + clientId: 'client-id-parent-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses correct specificity in double-layered parent block', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/nested-block': { + 'core/nested-block': { + 'core/child-block': { + color: { + text: true, + }, + }, + }, + 'core/child-block': { + color: { + text: false, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/nested-block { + // core/nested-block { + // core/child-block <- current block + // } + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/nested-block', + clientId: 'client-id-nested-block-1', + } ); + + mockBlockParent( 'client-id-nested-block-1', { + name: 'core/nested-block', + clientId: 'client-id-nested-block-2', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses correct specificity out of double-layered parent block', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/nested-block': { + 'core/nested-block': { + 'core/child-block': { + color: { + text: false, + }, + }, + }, + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/nested-block { + // core/child-block <- current block + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/nested-block', + clientId: 'client-id-nested-block', + } ); + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'ignores unrelated nested settings', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/unrelated-block': { + 'core/child-block': { + color: { + text: false, + }, + }, + }, + 'core/child-block': { + color: { + text: true, + }, + }, + }, + } ); + + // Mock editor structure: + // core/child-block <- current block + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + + it( 'uses the last nested settings value at the same depth', () => { + mockSettings( { + color: { + text: false, + }, + blocks: { + 'core/grandparent-block': { + 'core/child-block': { + color: { + text: false, + }, + }, + }, + 'core/parent-block': { + 'core/child-block': { + color: { + text: true, + }, + }, + }, + }, + } ); + + // Mock editor structure: + // core/grandparent-block { + // core/parent-block { + // core/child-block <- current block + // } + // } + + mockCurrentBlockContext( { + name: 'core/child-block', + clientId: 'client-id-child-block', + } ); + + mockBlockParent( 'client-id-child-block', { + name: 'core/parent-block', + clientId: 'client-id-parent-block', + } ); + + mockBlockParent( 'client-id-parent-block', { + name: 'core/grandparent-block', + clientId: 'client-id-grandparent-block', + } ); + + expect( useSetting( 'color.text' ) ).toBe( true ); + } ); + } ); +} ); diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index e3704bfdc864f6..1443979872deb9 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -48,6 +48,102 @@ function filter_db_query( $query ) { return $query; } + function test_theme_with_nested_block_settings() { + switch_theme( 'nested-block-theme' ); + + $actual_settings = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_settings(); + $expected_settings = array( + 'color' => array( + 'custom' => false, + 'customGradient' => true, + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#000', + ), + ), + ), + ), + 'typography' => array( + 'customFontSize' => true, + 'lineHeight' => false, + ), + 'spacing' => array( + 'units' => false, + 'padding' => false, + ), + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + ), + ), + ), + ), + 'core/columns' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + ), + ), + ), + 'core/media-text' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#000', + ), + ), + ), + ), + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + ), + ), + ), + ), + ), + ), + ), + ); + + self::recursive_ksort( $actual_settings ); + self::recursive_ksort( $expected_settings ); + + $this->assertSame( + $expected_settings, + $actual_settings + ); + } + function test_translations_are_applied() { add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); load_textdomain( 'block-theme', realpath( __DIR__ . '/data/languages/themes/block-theme-pl_PL.mo' ) ); diff --git a/phpunit/data/themedir1/nested-block-theme/style.css b/phpunit/data/themedir1/nested-block-theme/style.css new file mode 100644 index 00000000000000..39e1a1670afc6a --- /dev/null +++ b/phpunit/data/themedir1/nested-block-theme/style.css @@ -0,0 +1,8 @@ +/* +Theme Name: Nested Blocks Theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Template: block-theme +Version: 1.0.0 +Text Domain: nested-block-theme +*/ diff --git a/phpunit/data/themedir1/nested-block-theme/theme.json b/phpunit/data/themedir1/nested-block-theme/theme.json new file mode 100644 index 00000000000000..0eb4b953667253 --- /dev/null +++ b/phpunit/data/themedir1/nested-block-theme/theme.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "settings": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + }, + { + "slug": "dark", + "name": "Dark", + "color": "#000" + } + ], + "custom": false + }, + "blocks": { + "core/columns": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + } + ] + }, + "core/media-text": { + "color": { + "palette": [ + { + "slug": "dark", + "name": "Dark", + "color": "#000" + } + ] + }, + "core/paragraph": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + } + ] + } + } + } + } + } + }, + "customTemplates": [ + { + "name": "page-home", + "title": "Homepage template" + } + ], + "templateParts": [ + { + "name": "small-header", + "title": "Small Header", + "area": "header" + } + ] +} diff --git a/test/e2e/specs/editor/blocks/nestedBlocks.spec.js b/test/e2e/specs/editor/blocks/nestedBlocks.spec.js new file mode 100644 index 00000000000000..e940e333127844 --- /dev/null +++ b/test/e2e/specs/editor/blocks/nestedBlocks.spec.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Nested Block Settings', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should output a quote with a heading, that only allows red coloured text', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/quote', + innerBlocks: [ + { + name: 'core/heading', + }, + ], + } ); + + await page.click( '[data-type="core/heading"]' ); + + await page.keyboard.type( 'hello' ); + + await page.click( 'role=button[name="Text"i]' ); + + await expect( + page.locator( "[aria-label='Color: Red']" ) + ).toBeVisible(); + + await expect( + page.locator( "[aria-label='Color: Light']" ) + ).toBeHidden(); + + const editedPostContent = await editor.getEditedPostContent(); + expect( editedPostContent ).toBe( + ` +
++` + ); + } ); +} ); diff --git a/test/emptytheme/theme.json b/test/emptytheme/theme.json index ed2d7b8d0946aa..0338bd39cd64f5 100644 --- a/test/emptytheme/theme.json +++ b/test/emptytheme/theme.json @@ -2,9 +2,39 @@ "version": 2, "settings": { "appearanceTools": true, + "color": { + "palette": [ + { + "slug": "red", + "name": "Red", + "color": "#ce0808" + }, + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + } + ] + }, "layout": { "contentSize": "840px", "wideSize": "1100px" + }, + "blocks": { + "core/quote": { + "core/heading": { + "color": { + "text": true, + "palette": [ + { + "slug": "red", + "name": "Red", + "color": "#ce0808" + } + ] + } + } + } } }, "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ]hello
+