diff --git a/packages/e2e-test-utils/src/toggle-more-menu.js b/packages/e2e-test-utils/src/toggle-more-menu.js index fb7849c7990c91..67346e96cd21fa 100644 --- a/packages/e2e-test-utils/src/toggle-more-menu.js +++ b/packages/e2e-test-utils/src/toggle-more-menu.js @@ -6,6 +6,22 @@ export async function toggleMoreMenu( waitFor ) { const menuSelector = '.interface-more-menu-dropdown [aria-label="Options"]'; + const menuToggle = await page.waitForSelector( menuSelector ); + + const isOpen = await menuToggle.evaluate( ( el ) => + el.getAttribute( 'aria-expanded' ) + ); + + // If opening and it's already open then exit early. + if ( isOpen === 'true' && waitFor === 'open' ) { + return; + } + + // If closing and it's already closed then exit early. + if ( isOpen === 'false' && waitFor === 'close' ) { + return; + } + await page.click( menuSelector ); if ( waitFor ) { diff --git a/packages/edit-post/src/components/text-editor/index.js b/packages/edit-post/src/components/text-editor/index.js index b4b2ad64133a82..871273c4a9e83c 100644 --- a/packages/edit-post/src/components/text-editor/index.js +++ b/packages/edit-post/src/components/text-editor/index.js @@ -3,13 +3,14 @@ */ import { PostTextEditor, - PostTitle, + PostTitleRaw, store as editorStore, } from '@wordpress/editor'; import { Button } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { displayShortcut } from '@wordpress/keycodes'; +import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +23,23 @@ export default function TextEditor() { }, [] ); const { switchEditorMode } = useDispatch( editPostStore ); + const { isWelcomeGuideVisible } = useSelect( ( select ) => { + const { isFeatureActive } = select( editPostStore ); + + return { + isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), + }; + }, [] ); + + const titleRef = useRef(); + + useEffect( () => { + if ( isWelcomeGuideVisible ) { + return; + } + titleRef?.current?.focus(); + }, [ isWelcomeGuideVisible ] ); + return (
{ isRichEditingEnabled && ( @@ -37,7 +55,7 @@ export default function TextEditor() {
) }
- +
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index a3dfc991523225..5fefc5506a02fc 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -67,6 +67,7 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; +export { default as PostTitleRaw } from './post-title/post-title-raw'; export { default as PostTrash } from './post-trash'; export { default as PostTrashCheck } from './post-trash/check'; export { default as PostTypeSupportCheck } from './post-type-support-check'; diff --git a/packages/editor/src/components/post-title/constants.js b/packages/editor/src/components/post-title/constants.js new file mode 100644 index 00000000000000..2b0ff197f2b9f1 --- /dev/null +++ b/packages/editor/src/components/post-title/constants.js @@ -0,0 +1,4 @@ +export const DEFAULT_CLASSNAMES = + 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text'; + +export const REGEXP_NEWLINES = /[\r\n]+/g; diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index 09f5f30c2a660c..a61bc2f52eb842 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -7,18 +7,12 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from '@wordpress/element'; +import { forwardRef, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; -import { ENTER } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; -import { pasteHandler } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { ENTER } from '@wordpress/keycodes'; +import { pasteHandler } from '@wordpress/blocks'; import { __unstableUseRichText as useRichText, create, @@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies */ -import PostTypeSupportCheck from '../post-type-support-check'; import { store as editorStore } from '../../store'; - -/** - * Constants - */ -const REGEXP_NEWLINES = /[\r\n]+/g; +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; +import PostTypeSupportCheck from '../post-type-support-check'; function PostTitle( _, forwardedRef ) { - const ref = useRef(); + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + title: getEditedPostAttribute( 'title' ), + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + const [ isSelected, setIsSelected ] = useState( false ); - const { editPost } = useDispatch( editorStore ); - const { insertDefaultBlock, clearSelectedBlock, insertBlocks } = - useDispatch( blockEditorStore ); - const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect( - ( select ) => { - const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } = - select( editorStore ); - const { getSettings } = select( blockEditorStore ); - const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = - getSettings(); - - return { - isCleanNewPost: _isCleanNewPost(), - title: getEditedPostAttribute( 'title' ), - placeholder: titlePlaceholder, - hasFixedToolbar: _hasFixedToolbar, - }; - }, - [] - ); - useImperativeHandle( forwardedRef, () => ( { - focus: () => { - ref?.current?.focus(); - }, - } ) ); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); - useEffect( () => { - if ( ! ref.current ) { - return; - } + const { title, setTitle: onUpdate } = usePostTitle(); - const { defaultView } = ref.current.ownerDocument; - const { name, parent } = defaultView; - const ownerDocument = - name === 'editor-canvas' ? parent.document : defaultView.document; - const { activeElement, body } = ownerDocument; - - // Only autofocus the title when the post is entirely empty. This should - // only happen for a new post, which means we focus the title on new - // post so the author can start typing right away, without needing to - // click anything. - if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { - ref.current.focus(); - } - }, [ isCleanNewPost ] ); + const [ selection, setSelection ] = useState( {} ); - function onEnterPress() { - insertDefaultBlock( undefined, undefined, 0 ); + const { clearSelectedBlock, insertBlocks, insertDefaultBlock } = + useDispatch( blockEditorStore ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); } function onInsertBlockAfter( blocks ) { insertBlocks( blocks, 0 ); } - function onUpdate( newTitle ) { - editPost( { title: newTitle } ); - } - - const [ selection, setSelection ] = useState( {} ); - function onSelect() { setIsSelected( true ); clearSelectedBlock(); @@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) { setSelection( {} ); } - function onChange( value ) { - onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + function onEnterPress() { + insertDefaultBlock( undefined, undefined, 0 ); } function onKeyDown( event ) { @@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) { ( firstBlock.name === 'core/heading' || firstBlock.name === 'core/paragraph' ) ) { - onUpdate( stripHTML( firstBlock.attributes.content ) ); + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( + firstBlock.attributes.content + ); + onUpdate( contentNoHTML ); onInsertBlockAfter( content.slice( 1 ) ); } else { onInsertBlockAfter( content ); @@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) { ...create( { html: title } ), ...selection, }; - const newValue = insert( - value, - create( { html: stripHTML( content ) } ) - ); + + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( content ); + + const newValue = insert( value, create( { html: contentNoHTML } ) ); onUpdate( toHTMLString( { value: newValue } ) ); setSelection( { start: newValue.start, @@ -192,21 +162,13 @@ function PostTitle( _, forwardedRef ) { } } - // The wp-block className is important for editor styles. - // This same block is used in both the visual and the code editor. - const className = classnames( - 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text', - { - 'is-selected': isSelected, - 'has-fixed-toolbar': hasFixedToolbar, - } - ); const decodedPlaceholder = decodeEntities( placeholder ) || __( 'Add title' ); + const { ref: richTextRef } = useRichText( { value: title, onChange, - placeholder: decodedPlaceholder, + decodedPlaceholder, selectionStart: selection.start, selectionEnd: selection.end, onSelectionChange( newStart, newEnd ) { @@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) { }; } ); }, - __unstableDisableFormats: true, + __unstableDisableFormats: false, + } ); + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, } ); - /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ return ( + /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */

+ /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ ); - /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ } export default forwardRef( PostTitle ); diff --git a/packages/editor/src/components/post-title/post-title-raw.js b/packages/editor/src/components/post-title/post-title-raw.js new file mode 100644 index 00000000000000..b6a52e43731926 --- /dev/null +++ b/packages/editor/src/components/post-title/post-title-raw.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { useState, forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; + +function PostTitleRaw( _, forwardedRef ) { + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + + const [ isSelected, setIsSelected ] = useState( false ); + + const { title, setTitle: onUpdate } = usePostTitle(); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + } + + function onSelect() { + setIsSelected( true ); + } + + function onUnselect() { + setIsSelected( false ); + } + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, + 'is-raw-text': true, + } ); + + const decodedPlaceholder = + decodeEntities( placeholder ) || __( 'Add title' ); + + return ( + + ); +} + +export default forwardRef( PostTitleRaw ); diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss new file mode 100644 index 00000000000000..bf667c39933bdf --- /dev/null +++ b/packages/editor/src/components/post-title/style.scss @@ -0,0 +1,4 @@ +.edit-post-text-editor__body .is-raw-text textarea { + font-size: inherit; + line-height: inherit; +} diff --git a/packages/editor/src/components/post-title/use-post-title-focus.js b/packages/editor/src/components/post-title/use-post-title-focus.js new file mode 100644 index 00000000000000..effac53f2670a2 --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title-focus.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { useEffect, useImperativeHandle, useRef } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitleFocus( forwardedRef ) { + const ref = useRef(); + + const { isCleanNewPost } = useSelect( ( select ) => { + const { isCleanNewPost: _isCleanNewPost } = select( editorStore ); + + return { + isCleanNewPost: _isCleanNewPost(), + }; + }, [] ); + + useImperativeHandle( forwardedRef, () => ( { + focus: () => { + ref?.current?.focus(); + }, + } ) ); + + useEffect( () => { + if ( ! ref.current ) { + return; + } + + const { defaultView } = ref.current.ownerDocument; + const { name, parent } = defaultView; + const ownerDocument = + name === 'editor-canvas' ? parent.document : defaultView.document; + const { activeElement, body } = ownerDocument; + + // Only autofocus the title when the post is entirely empty. This should + // only happen for a new post, which means we focus the title on new + // post so the author can start typing right away, without needing to + // click anything. + if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { + ref.current.focus(); + } + }, [ isCleanNewPost ] ); + + return { ref }; +} diff --git a/packages/editor/src/components/post-title/use-post-title.js b/packages/editor/src/components/post-title/use-post-title.js new file mode 100644 index 00000000000000..65bd67af6fb4c8 --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitle() { + const { editPost } = useDispatch( editorStore ); + const { title } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + + return { + title: getEditedPostAttribute( 'title' ), + }; + }, [] ); + + function updateTitle( newTitle ) { + editPost( { title: newTitle } ); + } + + return { title, setTitle: updateTitle }; +} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index ccc26e23b430b0..986cb645c271f5 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -17,6 +17,7 @@ @import "./components/post-sync-status/style.scss"; @import "./components/post-taxonomies/style.scss"; @import "./components/post-text-editor/style.scss"; +@import "./components/post-title/style.scss"; @import "./components/post-url/style.scss"; @import "./components/post-visibility/style.scss"; @import "./components/post-trash/style.scss"; diff --git a/test/e2e/specs/editor/various/post-title.spec.js b/test/e2e/specs/editor/various/post-title.spec.js new file mode 100644 index 00000000000000..3c2330d1684d03 --- /dev/null +++ b/test/e2e/specs/editor/various/post-title.spec.js @@ -0,0 +1,359 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Post title', () => { + test.describe( 'Focus handling', () => { + test( 'should focus on the post title field when creating a new post in visual mode', async ( { + editor, + admin, + } ) => { + await admin.createNewPost(); + + const pageTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toBeFocused(); + } ); + + test( 'should focus on the post title field when creating a new post in code editor mode', async ( { + page, + + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + // switch Editor to code editor mode + // Open code editor + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + // Check we're in Code view mode. + await expect( + page.getByRole( 'heading', { + name: 'Editing code', + } ) + ).toBeVisible(); + + const pageTitleField = page.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toBeFocused(); + } ); + } ); + test.describe( 'HTML handling', () => { + test( `should (visually) render any HTML in Post Editor's post title field when in Visual editing mode`, async ( { + page, + editor, + admin, + requestUtils, + } ) => { + const { id: postId } = await requestUtils.createPost( { + title: 'I am emphasis I am bold I am anchor', + content: 'Hello world', + status: 'publish', + } ); + + await admin.visitAdminPage( + 'post.php', + `post=${ postId }&action=edit` + ); + + await page.evaluate( () => { + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'welcomeGuide', false ); + + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'fullscreenMode', false ); + }, false ); + + const pageTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toHaveText( + 'I am emphasis I am bold I am anchor' + ); + + // Check the HTML elements have been **rendered** rather than + // output in raw form. + await expect( pageTitleField.locator( 'css=em' ) ).toHaveText( + 'emphasis' + ); + + await expect( pageTitleField.locator( 'css=strong' ) ).toHaveText( + 'bold' + ); + + await expect( pageTitleField.locator( 'css=a' ) ).toHaveText( + 'anchor' + ); + } ); + + test( `should show raw HTML in the post title field when in Code view mode `, async ( { + page, + admin, + requestUtils, + pageUtils, + } ) => { + const { id: postId } = await requestUtils.createPost( { + title: 'I am emphasis I am bold I am anchor', + content: 'Hello world', + status: 'publish', + } ); + + await admin.visitAdminPage( + 'post.php', + `post=${ postId }&action=edit` + ); + + await page.evaluate( () => { + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'welcomeGuide', false ); + + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'fullscreenMode', false ); + }, false ); + + // switch Editor to code editor mode + // Open code editor + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + // Check we're in Code view mode. + await expect( + page.getByRole( 'heading', { + name: 'Editing code', + } ) + ).toBeVisible(); + + const codeViewPageTitleField = page.getByRole( 'textbox', { + name: 'Add title', + } ); + + // Check that the pageTitleField has the raw HTML + await expect( codeViewPageTitleField ).toHaveText( + 'I am emphasis I am bold I am anchor' + ); + } ); + + test( 'should strip HTML tags when pasting string of HTML into the post title field in Visual mode', async ( { + editor, + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + const pageTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toBeFocused(); + + pageUtils.setClipboardData( { + html: 'I am emphasis I am bold I am anchor', + } ); + await pageUtils.pressKeys( 'primary+v' ); + + await expect( pageTitleField ).toHaveText( + 'I am emphasis I am bold I am anchor' + ); + + // Check the HTML elements have been stripped and are not rendered. + await expect( pageTitleField.locator( 'css=em' ) ).toBeHidden(); + + await expect( pageTitleField.locator( 'css=strong' ) ).toBeHidden(); + + await expect( pageTitleField.locator( 'css=a' ) ).toBeHidden(); + } ); + + // Reinstate once the PR to fix paste events is merged: + // https://github.com/WordPress/gutenberg/pull/55030. + // eslint-disable-next-line playwright/no-skipped-test + test.skip( 'should retain HTML tags when pasting string of HTML into the post title field in Code view mode', async ( { + page, + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + // switch Editor to code editor mode + // Open code editor + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + // Check we're in Code view mode. + await expect( + page.getByRole( 'heading', { + name: 'Editing code', + } ) + ).toBeVisible(); + + const pageTitleField = page.getByRole( 'textbox', { + name: 'Add title', + } ); + + pageUtils.setClipboardData( { + plainText: + 'I am emphasis I am bold I am anchor', + html: 'I am emphasis I am bold I am anchor', + } ); + + // focus on the title field + await pageTitleField.focus(); + + await pageUtils.pressKeys( 'primary+v' ); + + await expect( pageTitleField ).toHaveText( + 'I am emphasis I am bold I am anchor' + ); + } ); + + test( 'should strip HTML tags from Post Title when pasted text is transformed to blocks', async ( { + editor, + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + const pageTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toBeFocused(); + + // This HTML will ultimately be parsed into two blocks + // The first will have it's `content` attribute stripped of HTML + // and used as the Page Title. + // The second will be inserted into the post contents and will + // retain its HTML. + pageUtils.setClipboardData( { + html: ` +

I am heading block title with HTML tag

+

And I am the rest of titles with emphasis tag!

+ `, + } ); + await pageUtils.pressKeys( 'primary+v' ); + + // Check the HTML elements have been stripped from the first block's + // `content` attribute... + await expect( pageTitleField ).toHaveText( + 'I am heading block title with HTML tag' + ); + + // ...and are not rendered. + await expect( pageTitleField.locator( 'css=strong' ) ).toBeHidden(); + + // Check the 2nd block ended up in the post contents and did not + // have its HTML stripped out. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'And I am the rest of titles with emphasis tag!', + }, + }, + ] ); + } ); + + test( 'should output HTML tags in plaintext when added into Post Title field in visual editor mode', async ( { + editor, + page, + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + const pageTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Add title', + } ); + + await expect( pageTitleField ).toBeFocused(); + + await page.keyboard.type( 'I am emphasis' ); + + // Expect that manually inputting HTML does not result in any + // unexpected transformations into rendered output. + await expect( pageTitleField ).toHaveText( + 'I am emphasis' + ); + + // Check that the `em` tag was output in plaintext and not rendered. + await expect( pageTitleField.locator( 'css=em' ) ).toBeHidden(); + + // Switch to code view + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + const codeViewPageTitleField = page.getByRole( 'textbox', { + name: 'Add title', + } ); + + // Check that the `em` tag was output in plaintext (HTML entities) + // Note that the `>` is not required to be converted to entity form + // (see https://github.com/WordPress/gutenberg/pull/54718/files#r1347124685). + await expect( codeViewPageTitleField ).toHaveText( + 'I am <em>emphasis</em>' + ); + } ); + + test( 'should output HTML tags in plaintext in visual editor mode when HTML is added in plaintext in code editor mode', async ( { + editor, + page, + admin, + pageUtils, + } ) => { + await admin.createNewPost(); + + // switch Editor to code editor mode + // Open code editor + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + // Check we're in Code view mode. + await expect( + page.getByRole( 'heading', { + name: 'Editing code', + } ) + ).toBeVisible(); + + const codeViewPageTitleField = page.getByRole( 'textbox', { + name: 'Add title', + } ); + + await codeViewPageTitleField.focus(); + + // Also verifies that the field handles typing into the field. + await page.keyboard.type( 'I am <em>emphasis</em>' ); + + await expect( codeViewPageTitleField ).toHaveText( + 'I am <em>emphasis</em>' + ); + + // Switch to visual view + await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + + const visualViewPageTitleField = editor.canvas.getByRole( + 'textbox', + { + name: 'Add title', + editable: 'richtext', + } + ); + + // Check that the `em` tag was output in plaintext + await expect( visualViewPageTitleField ).toHaveText( + 'I am emphasis' + ); + + // Check that no HTML tags were rendered. + await expect( + visualViewPageTitleField.locator( 'css=em' ) + ).toBeHidden(); + } ); + } ); +} );