diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index 0629a39728602..ffbe50ab3cc90 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -8,67 +8,158 @@ */ /** -* Register custom fields and custom block bindings sources. -*/ + * Code necessary for testing block bindings: + * - Enqueues a custom script to register sources in the client. + * - Registers sources in the server. + * - Registers a custom post type and custom fields. + */ function gutenberg_test_block_bindings_registration() { + // Define fields list. + $upload_dir = wp_upload_dir(); + $testing_url = $upload_dir['url'] . '/1024x768_e2e_test_image_size.jpeg'; + $fields_list = array( + 'text_field' => array( + 'label' => 'Text Field Label', + 'value' => 'Text Field Value', + ), + 'url_field' => array( + 'label' => 'URL Field Label', + 'value' => $testing_url, + ), + 'empty_field' => array( + 'label' => 'Empty Field Label', + 'value' => '', + ), + ); + + // Enqueue a custom script for the plugin. + wp_enqueue_script( + 'gutenberg-test-block-bindings', + plugins_url( 'block-bindings/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-private-apis', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ), + true + ); + + // Pass data to the script. + wp_localize_script( + 'gutenberg-test-block-bindings', + 'testingBindings', + array( + 'fieldsList' => $fields_list, + ) + ); + // Register custom block bindings sources. register_block_bindings_source( - 'core/server-source', + 'testing/complete-source', + array( + 'label' => 'Complete Source', + 'get_value_callback' => function ( $source_args ) use ( $fields_list ) { + if ( ! isset( $source_args['key'] ) || ! isset( $fields_list[ $source_args['key'] ] ) ) { + return null; + } + return $fields_list[ $source_args['key'] ]['value']; }, + ) + ); + register_block_bindings_source( + 'testing/server-only-source', array( 'label' => 'Server Source', 'get_value_callback' => function () {}, ) ); - // Register custom fields. + // Register "movie" custom post type. + register_post_type( + 'movie', + array( + 'label' => 'Movie', + 'public' => true, + 'supports' => array( 'title', 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', 'thumbnail', 'custom-fields', 'post-formats' ), + 'has_archive' => true, + 'show_in_rest' => true, + ) + ); + + // Register global custom fields. register_meta( 'post', 'text_custom_field', array( + 'default' => 'Value of the text custom field', 'show_in_rest' => true, - 'type' => 'string', 'single' => true, - 'default' => 'Value of the text custom field', + 'type' => 'string', ) ); register_meta( 'post', 'url_custom_field', array( + 'default' => '#url-custom-field', 'show_in_rest' => true, - 'type' => 'string', 'single' => true, - 'default' => '#url-custom-field', + 'type' => 'string', ) ); + // Register CPT custom fields. register_meta( 'post', - 'empty_field', + 'movie_field', array( - 'show_in_rest' => true, - 'type' => 'string', - 'single' => true, - 'default' => '', + 'label' => 'Movie field label', + 'default' => 'Movie field default value', + 'object_subtype' => 'movie', + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + register_meta( + 'post', + 'field_with_only_label', + array( + 'label' => 'Field with only label', + 'object_subtype' => 'movie', + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + register_meta( + 'post', + 'field_without_label_or_default', + array( + 'object_subtype' => 'movie', + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', ) ); register_meta( 'post', '_protected_field', array( - 'type' => 'string', - 'show_in_rest' => true, - 'single' => true, - 'default' => 'protected field value', + 'default' => 'Protected field value', + 'object_subtype' => 'movie', + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', ) ); register_meta( 'post', 'show_in_rest_false_field', array( - 'show_in_rest' => false, - 'type' => 'string', - 'single' => true, - 'default' => 'show_in_rest false field value', + 'default' => 'show_in_rest false field value', + 'object_subtype' => 'movie', + 'show_in_rest' => false, + 'single' => true, + 'type' => 'string', ) ); } diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js new file mode 100644 index 0000000000000..dfdc706b69c64 --- /dev/null +++ b/packages/e2e-tests/plugins/block-bindings/index.js @@ -0,0 +1,55 @@ +const { unlock } = + wp.privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/blocks' + ); + +const { registerBlockBindingsSource } = unlock( wp.blocks.privateApis ); +const { fieldsList } = window.testingBindings || {}; + +const getValues = ( { bindings } ) => { + const newValues = {}; + for ( const [ attributeName, source ] of Object.entries( bindings ) ) { + newValues[ attributeName ] = fieldsList[ source.args.key ]?.value; + } + return newValues; +}; +const setValues = ( { registry, bindings } ) => { + Object.values( bindings ).forEach( ( { args, newValue } ) => { + // Example of what could be done. + registry.dispatch( 'core' ).editEntityRecord( 'postType', 'post', 1, { + meta: { [ args?.key ]: newValue }, + } ); + } ); +}; + +registerBlockBindingsSource( { + name: 'testing/complete-source', + label: 'Complete Source', + getValues, + setValues, + canUserEditValue: () => true, + getFieldsList: () => fieldsList, +} ); + +registerBlockBindingsSource( { + name: 'testing/can-user-edit-false', + label: 'Can User Edit: False', + getValues, + setValues, + canUserEditValue: () => false, +} ); + +registerBlockBindingsSource( { + name: 'testing/can-user-edit-undefined', + label: 'Can User Edit: Undefined', + getValues, + setValues, +} ); + +registerBlockBindingsSource( { + name: 'testing/set-values-undefined', + label: 'Set Values: Undefined', + getValues, + canUserEditValue: () => true, +} ); diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js deleted file mode 100644 index f172a424bb172..0000000000000 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ /dev/null @@ -1,2451 +0,0 @@ -/** - * External dependencies - */ -const path = require( 'path' ); -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); - -test.describe( 'Block bindings', () => { - let imagePlaceholderSrc; - let imageCustomFieldSrc; - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'emptytheme' ); - await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); - await requestUtils.deleteAllMedia(); - const placeholderMedia = await requestUtils.uploadMedia( - path.join( './test/e2e/assets', '10x10_e2e_test_image_z9T8jK.png' ) - ); - imagePlaceholderSrc = placeholderMedia.source_url; - } ); - - test.afterEach( async ( { requestUtils } ) => { - await requestUtils.deleteAllPosts(); - } ); - - test.afterAll( async ( { requestUtils } ) => { - await requestUtils.deleteAllMedia(); - await requestUtils.activateTheme( 'twentytwentyone' ); - await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); - } ); - - test.describe( 'Template context', () => { - test.beforeEach( async ( { admin, editor } ) => { - await admin.visitSiteEditor( { - postId: 'emptytheme//index', - postType: 'wp_template', - canvas: 'edit', - } ); - await editor.openDocumentSettingsSidebar(); - } ); - - test.describe( 'Paragraph', () => { - test( 'should show the key of the custom field in post meta', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'text_custom_field' - ); - } ); - - test( 'should always show the source label in server-only sources', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/server-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( 'Server Source' ); - } ); - - test( 'should lock the appropriate controls with a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await paragraphBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should lock the appropriate controls when source is not defined', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await paragraphBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - } ); - - test.describe( 'Heading', () => { - test( 'should show the key of the custom field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await expect( headingBlock ).toHaveText( 'text_custom_field' ); - } ); - - test( 'should lock the appropriate controls with a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await headingBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should lock the appropriate controls when source is not defined', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await headingBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - } ); - - test.describe( 'Button', () => { - test( 'should show the key of the custom field when text is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ); - await expect( buttonBlock ).toHaveText( 'text_custom_field' ); - } ); - - test( 'should lock text controls when text is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeVisible(); - } ); - - test( 'should lock text controls when text is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeVisible(); - } ); - - test( 'should lock url controls when url is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Format controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeVisible(); - - // Button is editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Format controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeVisible(); - - // Button is editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - - test( 'should lock url and text controls when both are bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls are visible. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - } ); - - test.describe( 'Image', () => { - test( 'should show the upload form when url is not bound', async ( { - editor, - } ) => { - await editor.insertBlock( { name: 'core/image' } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeVisible(); - } ); - - test( 'should NOT show the upload form when url is bound to a registered source', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - } ); - - test( 'should NOT show the upload form when url is bound to an undefined source', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - } ); - - test( 'should lock url controls when url is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable alt textarea when alt is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable alt textarea when alt is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'plguin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable title input when title is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is disabled and with the custom field value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'text_custom_field' ); - } ); - - test( 'should disable title input when title is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is disabled and with the custom field value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'Multiple bindings should lock the appropriate controls', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - } ); - } ); - - test.describe( 'Post/page context', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost( { title: 'Test bindings' } ); - } ); - test.describe( 'Paragraph', () => { - test( 'should show the value of the custom field when exists', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'Value of the text custom field' ); - } ); - - test( "should show the value of the key when custom field doesn't exist", async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'non_existing_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'non_existing_custom_field' - ); - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should prioritize the placeholder attribute over the placeholder generated by the bindings API', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - placeholder: 'My custom placeholder', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'empty_field' }, - }, - }, - }, - }, - } ); - - const paragraphBlock = editor.canvas.getByRole( 'document', { - // Aria-label is changed for empty paragraphs. - name: 'Empty empty_field; start writing to edit its value', - } ); - - await expect( paragraphBlock ).toBeEmpty(); - - const placeholder = paragraphBlock.locator( 'span' ); - await expect( placeholder ).toHaveAttribute( - 'data-rich-text-placeholder', - 'My custom placeholder' - ); - } ); - - test( 'should show the prompt placeholder in field with empty value', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'empty_field' }, - }, - }, - }, - }, - } ); - - const paragraphBlock = editor.canvas.getByRole( 'document', { - // Aria-label is changed for empty paragraphs. - name: 'Empty empty_field; start writing to edit its value', - } ); - - await expect( paragraphBlock ).toBeEmpty(); - - const placeholder = paragraphBlock.locator( 'span' ); - await expect( placeholder ).toHaveAttribute( - 'data-rich-text-placeholder', - 'Add empty_field' - ); - } ); - - test( 'should not show the value of a protected meta field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: '_protected_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( '_protected_field' ); - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should not show the value of a meta field with `show_in_rest` false', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'show_in_rest_false_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'show_in_rest_false_field' - ); - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should add empty paragraph block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - // Select the paragraph and press Enter at the end of it. - const paragraph = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await editor.selectBlocks( paragraph ); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - const [ initialParagraph, newEmptyParagraph ] = - await editor.canvas - .locator( '[data-type="core/paragraph"]' ) - .all(); - await expect( initialParagraph ).toHaveText( - 'Value of the text custom field' - ); - await expect( newEmptyParagraph ).toHaveText( '' ); - await expect( newEmptyParagraph ).toBeEditable(); - } ); - - test( 'should NOT be possible to edit the value of the custom field when it is protected', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'protected-field-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: '_protected_field' }, - }, - }, - }, - }, - } ); - - const protectedFieldBlock = editor.canvas.getByRole( - 'document', - { - name: 'Block: Paragraph', - } - ); - - await expect( protectedFieldBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should NOT be possible to edit the value of the custom field when it is not shown in the REST API', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'show-in-rest-false-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'show_in_rest_false_field' }, - }, - }, - }, - }, - } ); - - const showInRestFalseBlock = editor.canvas.getByRole( - 'document', - { - name: 'Block: Paragraph', - } - ); - - await expect( showInRestFalseBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - test( 'should show a selector for content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - } ); - await page.getByLabel( 'Attributes options' ).click(); - const contentAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show content', - } ); - await expect( contentAttribute ).toBeVisible(); - } ); - test( 'should use a selector to update the content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'undefined_field' }, - }, - }, - }, - }, - } ); - await page.getByRole( 'button', { name: 'content' } ).click(); - - await page - .getByRole( 'menuitemradio' ) - .filter( { hasText: 'text_custom_field' } ) - .click(); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'Value of the text custom field' - ); - } ); - } ); - - test.describe( 'Heading', () => { - test( 'should show the value of the custom field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - anchor: 'heading-binding', - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await expect( headingBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#heading-binding' ) - ).toHaveText( 'Value of the text custom field' ); - } ); - - test( 'should add empty paragraph block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - anchor: 'heading-binding', - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - - // Select the heading and press Enter at the end of it. - const heading = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await editor.selectBlocks( heading ); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor. - const [ initialHeading, newEmptyParagraph ] = - await editor.canvas.locator( '[data-block]' ).all(); - // First block should be the original block. - await expect( initialHeading ).toHaveAttribute( - 'data-type', - 'core/heading' - ); - await expect( initialHeading ).toHaveText( - 'Value of the text custom field' - ); - // Second block should be an empty paragraph block. - await expect( newEmptyParagraph ).toHaveAttribute( - 'data-type', - 'core/paragraph' - ); - await expect( newEmptyParagraph ).toHaveText( '' ); - await expect( newEmptyParagraph ).toBeEditable(); - } ); - test( 'should show a selector for content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - } ); - await page.getByLabel( 'Attributes options' ).click(); - const contentAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show content', - } ); - await expect( contentAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Button', () => { - test( 'should show the value of the custom field when text is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-text-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - await expect( buttonBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-text-binding a' - ); - await expect( buttonDom ).toHaveText( - 'Value of the text custom field' - ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#default-url' - ); - } ); - - test( 'should use the value of the custom field when url is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-url-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Check the frontend shows the original value of the custom field. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-url-binding a' - ); - await expect( buttonDom ).toHaveText( 'button default text' ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#url-custom-field' - ); - } ); - - test( 'should use the values of the custom fields when text and url are bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-multiple-bindings', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Check the frontend uses the values of the custom fields. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-multiple-bindings a' - ); - await expect( buttonDom ).toHaveText( - 'Value of the text custom field' - ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#url-custom-field' - ); - } ); - - test( 'should add empty button block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-text-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - await editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ) - .click(); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - const [ initialButton, newEmptyButton ] = await editor.canvas - .locator( '[data-type="core/button"]' ) - .all(); - // First block should be the original block. - await expect( initialButton ).toHaveText( - 'Value of the text custom field' - ); - // Second block should be an empty paragraph block. - await expect( newEmptyButton ).toHaveText( '' ); - await expect( newEmptyButton ).toBeEditable(); - } ); - test( 'should show a selector for url, text, linkTarget and rel', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - }, - ], - } ); - await editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ) - .click(); - await page - .getByRole( 'tabpanel', { - name: 'Settings', - } ) - .getByLabel( 'Attributes options' ) - .click(); - const urlAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show url', - } ); - await expect( urlAttribute ).toBeVisible(); - const textAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show text', - } ); - await expect( textAttribute ).toBeVisible(); - const linkTargetAttribute = page.getByRole( - 'menuitemcheckbox', - { - name: 'Show linkTarget', - } - ); - await expect( linkTargetAttribute ).toBeVisible(); - const relAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show rel', - } ); - await expect( relAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Image', () => { - test.beforeAll( async ( { requestUtils } ) => { - const customFieldMedia = await requestUtils.uploadMedia( - path.join( - './test/e2e/assets', - '1024x768_e2e_test_image_size.jpeg' - ) - ); - imageCustomFieldSrc = customFieldMedia.source_url; - } ); - - test.beforeEach( async ( { editor, page, requestUtils } ) => { - const postId = await editor.publishPost(); - await requestUtils.rest( { - method: 'POST', - path: '/wp/v2/posts/' + postId, - data: { - meta: { - url_custom_field: imageCustomFieldSrc, - }, - }, - } ); - await page.reload(); - } ); - test( 'should show the value of the custom field when url is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-url-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-url-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'default alt value' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - - test( 'should show value of the custom field in the alt textarea when alt is bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-alt-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the placeholder. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - - // Alt textarea should have the custom field value. - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-alt-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'Value of the text custom field' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - - test( 'should show value of the custom field in the title input when title is bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-title-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the placeholder. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - - // Title input should have the custom field value. - const advancedButton = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { - name: 'Advanced', - } ); - const isAdvancedPanelOpen = - await advancedButton.getAttribute( 'aria-expanded' ); - if ( isAdvancedPanelOpen === 'false' ) { - await advancedButton.click(); - } - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'Value of the text custom field' ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-title-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'default alt value' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'Value of the text custom field' - ); - } ); - - test( 'Multiple bindings should show the value of the custom fields', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-multiple-bindings', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the custom field value. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - - // Alt textarea should have the custom field value. - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); - - // Title input should have the original value. - const advancedButton = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { - name: 'Advanced', - } ); - const isAdvancedPanelOpen = - await advancedButton.getAttribute( 'aria-expanded' ); - if ( isAdvancedPanelOpen === 'false' ) { - await advancedButton.click(); - } - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - - // Check the frontend uses the values of the custom fields. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-multiple-bindings img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'Value of the text custom field' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - test( 'should show a selector for url, id, title and alt', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - } ); - await page - .getByRole( 'tabpanel', { - name: 'Settings', - } ) - .getByLabel( 'Attributes options' ) - .click(); - const urlAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show url', - } ); - await expect( urlAttribute ).toBeVisible(); - const idAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show id', - } ); - await expect( idAttribute ).toBeVisible(); - const titleAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show title', - } ); - await expect( titleAttribute ).toBeVisible(); - const altAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show alt', - } ); - await expect( altAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Edit custom fields', () => { - test( 'should be possible to edit the value of the custom field from the paragraph', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - await paragraphBlock.fill( 'new value' ); - // Check that the paragraph content attribute didn't change. - const [ paragraphBlockObject ] = await editor.getBlocks(); - expect( paragraphBlockObject.attributes.content ).toBe( - 'paragraph default content' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'new value' ); - } ); - - // Related issue: https://github.com/WordPress/gutenberg/issues/62347 - test( 'should be possible to use symbols and numbers as the custom field value', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - await paragraphBlock.fill( '$10.00' ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( '$10.00' ); - } ); - - test( 'should be possible to edit the value of the url custom field from the button', async ( { - editor, - page, - pageUtils, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-url-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Edit the url. - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - await page - .getByRole( 'button', { name: 'Edit link', exact: true } ) - .click(); - await page - .getByPlaceholder( 'Search or type URL' ) - .fill( '#url-custom-field-modified' ); - await pageUtils.pressKeys( 'Enter' ); - - // Check that the button url attribute didn't change. - const [ buttonsObject ] = await editor.getBlocks(); - expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe( - '#default-url' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#button-url-binding a' ) - ).toHaveAttribute( 'href', '#url-custom-field-modified' ); - } ); - - test( 'should be possible to edit the value of the url custom field from the image', async ( { - editor, - page, - pageUtils, - requestUtils, - } ) => { - const customFieldMedia = await requestUtils.uploadMedia( - path.join( - './test/e2e/assets', - '1024x768_e2e_test_image_size.jpeg' - ) - ); - imageCustomFieldSrc = customFieldMedia.source_url; - - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-url-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - - // Edit image url. - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - .click(); - await page - .getByRole( 'button', { name: 'Edit link', exact: true } ) - .click(); - await page - .getByPlaceholder( 'Search or type URL' ) - .fill( imageCustomFieldSrc ); - await pageUtils.pressKeys( 'Enter' ); - - // Check that the image url attribute didn't change and still uses the placeholder. - const [ imageBlockObject ] = await editor.getBlocks(); - expect( imageBlockObject.attributes.url ).toBe( - imagePlaceholderSrc - ); - - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#image-url-binding img' ) - ).toHaveAttribute( 'src', imageCustomFieldSrc ); - } ); - - test( 'should be possible to edit the value of the text custom field from the image alt', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-alt-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Edit the custom field value in the alt textarea. - const altInputArea = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ); - await expect( altInputArea ).not.toHaveAttribute( 'readonly' ); - await altInputArea.fill( 'new value' ); - - // Check that the image alt attribute didn't change. - const [ imageBlockObject ] = await editor.getBlocks(); - expect( imageBlockObject.attributes.alt ).toBe( - 'default alt value' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#image-alt-binding img' ) - ).toHaveAttribute( 'alt', 'new value' ); - } ); - - test( 'should not be possible to edit the value of the protected custom fields', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: '_protected_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - } ); - } ); - - test.describe( 'Sources registration', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost( { title: 'Test bindings' } ); - } ); - - test( 'should show the label of a source only registered in the server', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - metadata: { - bindings: { - content: { - source: 'core/server-source', - }, - }, - }, - }, - } ); - - const bindingsPanel = page.locator( - '.block-editor-bindings__panel' - ); - await expect( bindingsPanel ).toContainText( 'Server Source' ); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js new file mode 100644 index 0000000000000..d6563ce9cb5f5 --- /dev/null +++ b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js @@ -0,0 +1,1204 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Registered sources', () => { + let imagePlaceholderSrc; + let testingImgSrc; + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( + 'gutenberg-test-themes/block-bindings' + ); + await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); + await requestUtils.deleteAllMedia(); + const placeholderMedia = await requestUtils.uploadMedia( + path.join( './test/e2e/assets', '10x10_e2e_test_image_z9T8jK.png' ) + ); + imagePlaceholderSrc = placeholderMedia.source_url; + + const testingImgMedia = await requestUtils.uploadMedia( + path.join( + './test/e2e/assets', + '1024x768_e2e_test_image_size.jpeg' + ) + ); + testingImgSrc = testingImgMedia.source_url; + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { title: 'Test bindings' } ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.describe( 'getValues', () => { + test( 'should show the returned value in paragraph content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Text Field Value' ); + + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'Text Field Value' ); + } ); + test( 'should show the returned value in heading content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + anchor: 'connected-heading', + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await expect( headingBlock ).toHaveText( 'Text Field Value' ); + + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-heading' ) + ).toHaveText( 'Text Field Value' ); + } ); + test( 'should show the returned values in button attributes', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'connected-button', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + url: { + source: 'testing/complete-source', + args: { key: 'url_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Check the frontend uses the values of the custom fields. + const previewPage = await editor.openPreviewPage(); + const buttonDom = previewPage.locator( '#connected-button a' ); + await expect( buttonDom ).toHaveText( 'Text Field Value' ); + await expect( buttonDom ).toHaveAttribute( 'href', testingImgSrc ); + } ); + test( 'should show the returned values in image attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'connected-image', + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'testing/complete-source', + args: { key: 'url_field' }, + }, + alt: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const imageBlockImg = editor.canvas + .getByRole( 'document', { + name: 'Block: Image', + } ) + .locator( 'img' ); + await imageBlockImg.click(); + + // Image src is the custom field value. + await expect( imageBlockImg ).toHaveAttribute( + 'src', + testingImgSrc + ); + + // Alt textarea should have the custom field value. + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'Text Field Value' ); + + // Title input should have the original value. + const advancedButton = page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { + name: 'Advanced', + } ); + const isAdvancedPanelOpen = + await advancedButton.getAttribute( 'aria-expanded' ); + if ( isAdvancedPanelOpen === 'false' ) { + await advancedButton.click(); + } + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + + // Check the frontend uses the values of the custom fields. + const previewPage = await editor.openPreviewPage(); + const imageDom = previewPage.locator( '#connected-image img' ); + await expect( imageDom ).toHaveAttribute( 'src', testingImgSrc ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'Text Field Value' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'default title value' + ); + } ); + test( 'should fall back to source label when `getValues` is undefined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/server-only-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Server Source' ); + } ); + test( 'should fall back to null when `getValues` is undefined in URL attributes', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + metadata: { + bindings: { + url: { + source: 'testing/server-only-source', + args: { key: 'url_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await expect( + imageBlock.locator( '.components-placeholder__fieldset' ) + ).toHaveText( 'Connected to Server Source' ); + } ); + } ); + + test.describe( 'should lock editing', () => { + // Logic reused accross all the tests that check paragraph editing is locked. + async function testParagraphControlsAreLocked( { + source, + editor, + page, + } ) { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source, + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await paragraphBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Paragraph is not editable. + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } + test.describe( 'canUserEditValue returns false', () => { + test( 'paragraph', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/can-user-edit-false', + editor, + page, + } ); + } ); + test( 'heading', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await headingBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Heading is not editable. + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'button', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + url: { + source: 'testing/can-user-edit-false', + args: { key: 'url_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Alignment controls are visible. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Button is not editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Link controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Link' } ) + ).toBeHidden(); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeHidden(); + } ); + test( 'image', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'testing/can-user-edit-false', + args: { key: 'url_field' }, + }, + alt: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + title: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeHidden(); + + // Image placeholder doesn't show the upload button. + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + + // Alt textarea is disabled and with the custom field value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toHaveAttribute( 'readonly' ); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'Text Field Value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toHaveAttribute( 'readonly' ); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'Text Field Value' ); + } ); + } ); + // The following tests just check the paragraph and assume is the case for the rest of the blocks. + test( 'canUserEditValue is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/can-user-edit-undefined', + editor, + page, + } ); + } ); + test( 'setValues is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/complete-source-undefined', + editor, + page, + } ); + } ); + test( 'source is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/undefined-source', + editor, + page, + } ); + } ); + } ); + + // Use `core/post-meta` source to test editing to avoid overcomplicating custom sources. + // It needs a source that can be consumed and edited from the server and the editor. + test.describe( 'setValues', () => { + test( 'should be possible to edit the value from paragraph content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'paragraph default content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + // Related issue: https://github.com/WordPress/gutenberg/issues/62347 + test( 'should be possible to use symbols and numbers as the custom field value', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'paragraph-binding', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( '$10.00' ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#paragraph-binding' ) + ).toHaveText( '$10.00' ); + } ); + test( 'should be possible to edit the value of the url custom field from the button', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'button-url-binding', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Edit the url. + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type URL' ) + .fill( '#url-custom-field-modified' ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the button url attribute didn't change. + const [ buttonsObject ] = await editor.getBlocks(); + expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe( + '#default-url' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#button-url-binding a' ) + ).toHaveAttribute( 'href', '#url-custom-field-modified' ); + } ); + test( 'should be possible to edit the value of the url custom field from the image', async ( { + editor, + page, + pageUtils, + requestUtils, + } ) => { + const customFieldMedia = await requestUtils.uploadMedia( + path.join( + './test/e2e/assets', + '1024x768_e2e_test_image_size.jpeg' + ) + ); + testingImgSrc = customFieldMedia.source_url; + + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-url-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + + // Edit image url. + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + .click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type URL' ) + .fill( testingImgSrc ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the image url attribute didn't change and still uses the placeholder. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.url ).toBe( + imagePlaceholderSrc + ); + + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-url-binding img' ) + ).toHaveAttribute( 'src', testingImgSrc ); + } ); + test( 'should be possible to edit the value of the text custom field from the image alt', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-alt-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + metadata: { + bindings: { + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlockImg = editor.canvas + .getByRole( 'document', { + name: 'Block: Image', + } ) + .locator( 'img' ); + await imageBlockImg.click(); + + // Edit the custom field value in the alt textarea. + const altInputArea = page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ); + await expect( altInputArea ).not.toHaveAttribute( 'readonly' ); + await altInputArea.fill( 'new value' ); + + // Check that the image alt attribute didn't change. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.alt ).toBe( + 'default alt value' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-alt-binding img' ) + ).toHaveAttribute( 'alt', 'new value' ); + } ); + } ); + + test.describe( 'getFieldsList', () => { + test( 'should be possible to update attribute value through bindings UI', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page.getByRole( 'button', { name: 'content' } ).click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ) + .click(); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Text Field Value' ); + } ); + test( 'should be possible to connect the paragraph content', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + const contentAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ); + await expect( contentAttribute ).toBeVisible(); + } ); + test( 'should be possible to connect the heading content', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + } ); + await page.getByLabel( 'Attributes options' ).click(); + const contentAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ); + await expect( contentAttribute ).toBeVisible(); + } ); + test( 'should be possible to connect the button supported attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + }, + ], + } ); + await editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ) + .click(); + await page + .getByRole( 'tabpanel', { + name: 'Settings', + } ) + .getByLabel( 'Attributes options' ) + .click(); + const urlAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show url', + } ); + await expect( urlAttribute ).toBeVisible(); + const textAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show text', + } ); + await expect( textAttribute ).toBeVisible(); + const linkTargetAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show linkTarget', + } ); + await expect( linkTargetAttribute ).toBeVisible(); + const relAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show rel', + } ); + await expect( relAttribute ).toBeVisible(); + // Check not supported attributes are not included. + const tagNameAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show tagName', + } ); + await expect( tagNameAttribute ).toBeHidden(); + } ); + test( 'should be possible to connect the image supported attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + } ); + await page + .getByRole( 'tabpanel', { + name: 'Settings', + } ) + .getByLabel( 'Attributes options' ) + .click(); + const urlAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show url', + } ); + await expect( urlAttribute ).toBeVisible(); + const idAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show id', + } ); + await expect( idAttribute ).toBeVisible(); + const titleAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show title', + } ); + await expect( titleAttribute ).toBeVisible(); + const altAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show alt', + } ); + await expect( altAttribute ).toBeVisible(); + // Check not supported attributes are not included. + const linkClassAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show linkClass', + } ); + await expect( linkClassAttribute ).toBeHidden(); + } ); + test( 'should show all the available fields in the dropdown UI', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'default value', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + await page.getByRole( 'button', { name: 'content' } ).click(); + const textField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ); + await expect( textField ).toBeVisible(); + await expect( textField ).toBeChecked(); + const urlField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'URL Field Label' } ); + await expect( urlField ).toBeVisible(); + await expect( urlField ).not.toBeChecked(); + } ); + test( 'should show the connected fields in the attributes panel', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'default value', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Text Field Label' ); + } ); + } ); + + test.describe( 'RichText workflows', () => { + test( 'should add empty paragraph block when pressing enter in paragraph', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + // Select the paragraph and press Enter at the end of it. + const paragraph = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await editor.selectBlocks( paragraph ); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + const [ initialParagraph, newEmptyParagraph ] = await editor.canvas + .locator( '[data-type="core/paragraph"]' ) + .all(); + await expect( initialParagraph ).toHaveText( 'Text Field Value' ); + await expect( newEmptyParagraph ).toHaveText( '' ); + await expect( newEmptyParagraph ).toBeEditable(); + } ); + test( 'should add empty paragraph block when pressing enter in heading', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + anchor: 'heading-binding', + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + + // Select the heading and press Enter at the end of it. + const heading = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await editor.selectBlocks( heading ); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor. + const [ initialHeading, newEmptyParagraph ] = await editor.canvas + .locator( '[data-block]' ) + .all(); + // First block should be the original block. + await expect( initialHeading ).toHaveAttribute( + 'data-type', + 'core/heading' + ); + await expect( initialHeading ).toHaveText( 'Text Field Value' ); + // Second block should be an empty paragraph block. + await expect( newEmptyParagraph ).toHaveAttribute( + 'data-type', + 'core/paragraph' + ); + await expect( newEmptyParagraph ).toHaveText( '' ); + await expect( newEmptyParagraph ).toBeEditable(); + } ); + test( 'should add empty button block when pressing enter in button', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'button-text-binding', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + }, + ], + } ); + await editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ) + .click(); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + const [ initialButton, newEmptyButton ] = await editor.canvas + .locator( '[data-type="core/button"]' ) + .all(); + // First block should be the original block. + await expect( initialButton ).toHaveText( 'Text Field Value' ); + // Second block should be an empty paragraph block. + await expect( newEmptyButton ).toHaveText( '' ); + await expect( newEmptyButton ).toBeEditable(); + } ); + test( 'should show placeholder prompt when value is empty and can edit', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'Empty empty_field; start writing to edit its value', + } ); + + await expect( paragraphBlock ).toBeEmpty(); + + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'Add Empty Field Label' + ); + } ); + test( 'should show source label when value is empty, cannot edit, and `getFieldsList` is undefined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/can-user-edit-false', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'empty_field', + } ); + await expect( paragraphBlock ).toBeEmpty(); + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'Can User Edit: False' + ); + } ); + test( 'should show placeholder attribute over bindings placeholder', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + placeholder: 'My custom placeholder', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'empty_field', + } ); + + await expect( paragraphBlock ).toBeEmpty(); + + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'My custom placeholder' + ); + } ); + } ); + + test( 'should show the label of a source only registered in the server in blocks connected', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + metadata: { + bindings: { + content: { + source: 'testing/server-only-source', + }, + }, + }, + }, + } ); + + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Server Source' ); + } ); + test( 'should show an "Invalid source" warning for not registered sources', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + metadata: { + bindings: { + content: { + source: 'testing/undefined-source', + }, + }, + }, + }, + } ); + + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Invalid source' ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js new file mode 100644 index 0000000000000..d82def6feb66b --- /dev/null +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -0,0 +1,551 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Post Meta source', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( + 'gutenberg-test-themes/block-bindings' + ); + await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.describe( 'Movie CPT template', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor( { + postId: 'gutenberg-test-themes/block-bindings//single-movie', + postType: 'wp_template', + canvas: 'edit', + } ); + await editor.openDocumentSettingsSidebar(); + } ); + + test.describe( 'Block attributes values', () => { + test( 'should not be possible to edit connected blocks', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should show the default value if it is defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + } ); + test( 'should fall back to the field label if the default value is not defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'field_with_only_label', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Field with only label' + ); + } ); + test( 'should fall back to the field key if the field label is not defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'field_without_label_or_default', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'field_without_label_or_default' + ); + } ); + } ); + + test.describe( 'Attributes panel', () => { + test( 'should show the field label if it is defined', async ( { + editor, + page, + } ) => { + /** + * Create connection manually until this issue is solved: + * https://github.com/WordPress/gutenberg/pull/65604 + * + * Once solved, block with the binding can be directly inserted. + */ + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + const contentBinding = page.getByRole( 'button', { + name: 'content', + } ); + await contentBinding.click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ) + .click(); + await expect( contentBinding ).toContainText( + 'Movie field label' + ); + } ); + test( 'should fall back to the field key if the field label is not defined', async ( { + editor, + page, + } ) => { + /** + * Create connection manually until this issue is solved: + * https://github.com/WordPress/gutenberg/pull/65604 + * + * Once solved, block with the binding can be directly inserted. + */ + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + const contentBinding = page.getByRole( 'button', { + name: 'content', + } ); + await contentBinding.click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'field_without_label_or_default' } ) + .click(); + await expect( contentBinding ).toContainText( + 'field_without_label_or_default' + ); + } ); + } ); + + test.describe( 'Fields list dropdown', () => { + // Insert block and open the dropdown for every test. + test.beforeEach( async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + } ); + + test( 'should include movie fields in UI to connect attributes', async ( { + page, + } ) => { + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeVisible(); + } ); + test( 'should include global fields in UI to connect attributes', async ( { + page, + } ) => { + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeVisible(); + } ); + test( 'should not include protected fields', async ( { page } ) => { + // Ensure the fields have loaded by checking the field is visible. + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeVisible(); + // Check the protected fields are not visible. + const protectedField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: '_protected_field' } ); + await expect( protectedField ).toBeHidden(); + const showInRestFalseField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'show_in_rest_false_field' } ); + await expect( showInRestFalseField ).toBeHidden(); + } ); + test( 'should show the default value if it is defined', async ( { + page, + } ) => { + const fieldButton = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( fieldButton ).toContainText( + 'Movie field default value' + ); + } ); + test( 'should not show anything if the default value is not defined', async ( { + page, + } ) => { + const fieldButton = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Field with only label' } ); + // Check it only contains the field label. + await expect( fieldButton ).toHaveText( + 'Field with only label' + ); + } ); + } ); + } ); + + test.describe( 'Custom template', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor( { + postId: 'gutenberg-test-themes/block-bindings//custom-template', + postType: 'wp_template', + canvas: 'edit', + } ); + await editor.openDocumentSettingsSidebar(); + } ); + + test( 'should not include post meta fields in UI to connect attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'text_custom_field', + }, + }, + }, + }, + }, + } ); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + // Check the fields registered by other sources are there. + const customSourceField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ); + await expect( customSourceField ).toBeVisible(); + // Check the post meta fields are not visible. + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeHidden(); + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeHidden(); + } ); + test( 'should show the key in attributes connected to post meta', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'text_custom_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'text_custom_field' ); + } ); + } ); + + test.describe( 'Movie CPT post', () => { + test.beforeEach( async ( { admin } ) => { + // CHECK HOW TO CREATE A MOVIE. + await admin.createNewPost( { + postType: 'movie', + title: 'Test bindings', + } ); + } ); + + test( 'should show the custom field value of that specific post', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'Movie field default value' ); + } ); + test( 'should fall back to the key when custom field is not accessible', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'unaccessible_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'unaccessible_field' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should not show or edit the value of a protected field', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: '_protected_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( '_protected_field' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should not show or edit the value of a field with `show_in_rest` set to false', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'show_in_rest_false_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'show_in_rest_false_field' + ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should be possible to edit the value of the connected custom fields', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'fallback content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + test( 'should be possible to connect movie fields through the attributes panel', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeVisible(); + } ); + } ); +} ); diff --git a/test/gutenberg-test-themes/block-bindings/index.php b/test/gutenberg-test-themes/block-bindings/index.php new file mode 100644 index 0000000000000..0c6530acc1aaf --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/index.php @@ -0,0 +1,9 @@ + +

Custom template

+ diff --git a/test/gutenberg-test-themes/block-bindings/templates/index.html b/test/gutenberg-test-themes/block-bindings/templates/index.html new file mode 100644 index 0000000000000..ab136ac8df9a7 --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/index.html @@ -0,0 +1,8 @@ + +
+ + + + +
+ diff --git a/test/gutenberg-test-themes/block-bindings/templates/single-movie.html b/test/gutenberg-test-themes/block-bindings/templates/single-movie.html new file mode 100644 index 0000000000000..cd05d5fe917fe --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/single-movie.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/block-bindings/templates/single.html b/test/gutenberg-test-themes/block-bindings/templates/single.html new file mode 100644 index 0000000000000..cd05d5fe917fe --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/single.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/block-bindings/theme.json b/test/gutenberg-test-themes/block-bindings/theme.json new file mode 100644 index 0000000000000..c996b01432839 --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/theme.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../schemas/json/theme.json", + "version": 3, + "settings": { + "appearanceTools": true, + "layout": { + "contentSize": "840px", + "wideSize": "1100px" + } + }, + "customTemplates": [ + { + "name": "custom-template", + "title": "Custom", + "postTypes": [ "post", "movie" ] + } + ] +}