diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap deleted file mode 100644 index 541a5456fd4d53..00000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Links allows use of escape key to dismiss the url popover 1`] = ` -" -

This is Gutenberg.

-" -`; - -exports[`Links can be created by selecting text and clicking Link 1`] = ` -" -

This is Gutenberg

-" -`; - -exports[`Links can be created instantly when a URL is selected 1`] = ` -" -

This is Gutenberg: https://wordpress.org/gutenberg

-" -`; - -exports[`Links can be created without any text selected 1`] = ` -" -

This is Gutenberg: https://wordpress.org/gutenberg

-" -`; - -exports[`Links can be edited 1`] = ` -" -

This is Gutenberg

-" -`; - -exports[`Links can be edited with collapsed selection 1`] = ` -" -

This is Gutenberg

-" -`; - -exports[`Links can be modified using the keyboard once a link has been set 1`] = ` -" -

This is Gutenberg.

-" -`; - -exports[`Links can be removed 1`] = ` -" -

This is Gutenberg

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js deleted file mode 100644 index 719d00afe076bb..00000000000000 --- a/packages/e2e-tests/specs/editor/various/links.test.js +++ /dev/null @@ -1,916 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - clickBlockToolbarButton, - getEditedPostContent, - createNewPost, - pressKeyWithModifier, - showBlockToolbar, - pressKeyTimes, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'Links', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - const waitForURLFieldAutoFocus = async () => { - await page.waitForFunction( () => { - const input = document.querySelector( - '.block-editor-url-input__input' - ); - if ( input ) { - input.focus(); - return true; - } - return false; - } ); - }; - - it( 'will use Post title as link text if link to existing post is created without any text selected', async () => { - const titleText = 'Post to create a link to'; - await createPostWithTitle( titleText ); - - await createNewPost(); - await clickBlockAppender(); - - // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. - await page.keyboard.type( 'Here comes a link: ' ); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Trigger the autocomplete suggestion list and select the first suggestion. - await page.keyboard.type( titleText.substr( 0, titleText.length - 2 ) ); - await page.waitForSelector( '.block-editor-link-control__search-item' ); - await page.keyboard.press( 'ArrowDown' ); - - await page.keyboard.press( 'Enter' ); - - const actualText = await canvas().evaluate( - () => - document.querySelector( '.block-editor-rich-text__editable a' ) - .textContent - ); - expect( actualText ).toBe( titleText ); - } ); - - it( 'can be created by selecting text and clicking Link', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - - // Select some text. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Submit the link. - await page.keyboard.press( 'Enter' ); - - // The link should have been inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'will not automatically create a link if selected text is not a valid HTTP based URL', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This: is not a link' ); - - // Select some text. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - const urlInputValue = await page.evaluate( - () => - document.querySelector( '.block-editor-url-input__input' ).value - ); - - expect( urlInputValue ).toBe( '' ); - } ); - - it( 'can be created without any text selected', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg: ' ); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Press Enter to apply the link. - await page.keyboard.press( 'Enter' ); - - // A link with the URL as its text should have been inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created instantly when a URL is selected', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( - 'This is Gutenberg: https://wordpress.org/gutenberg' - ); - - // Select the URL. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // A link with the selected URL as its href should have been inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'is not created when we click away from the link input', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - - // Select some text. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Click somewhere else - it doesn't really matter where. - await canvas().click( '.editor-post-title' ); - } ); - - const createAndReselectLink = async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - - // Select some text. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Click on the Submit button. - await page.keyboard.press( 'Enter' ); - - // Reselect the link. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - }; - - it( 'can be edited', async () => { - await createAndReselectLink(); - - // Click on the Edit button. - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - await editButton.click(); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Change the URL. - await page.keyboard.type( '/handbook' ); - - // Submit the link. - await page.keyboard.press( 'Enter' ); - - // The link should have been updated. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be removed', async () => { - await createAndReselectLink(); - - // Click on the Unlink button - // await page.click( 'button[aria-label="Unlink"]' ); - - // Unlick via shortcut - // we do this to avoid an layout edge case whereby - // the rich link preview popover will obscure the block toolbar - // under very specific circumstances and screensizes. - await pressKeyWithModifier( 'primaryShift', 'K' ); - - // The link should have been removed. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - const toggleFixedToolbar = async ( isFixed ) => { - await page.evaluate( ( _isFixed ) => { - const { select, dispatch } = wp.data; - const isCurrentlyFixed = - select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); - if ( isCurrentlyFixed !== _isFixed ) { - dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); - } - }, isFixed ); - }; - - it( 'allows Left to be pressed during creation when the toolbar is fixed to top', async () => { - await toggleFixedToolbar( true ); - - await clickBlockAppender(); - await page.keyboard.type( 'Text' ); - await page.click( 'button[aria-label="Link"]' ); - - // Typing "left" should not close the dialog. - await page.keyboard.press( 'ArrowLeft' ); - let popover = await page.$( - '.components-popover__content .block-editor-link-control' - ); - expect( popover ).not.toBeNull(); - - // Escape should close the dialog still. - await page.keyboard.press( 'Escape' ); - popover = await page.$( - '.components-popover__content .block-editor-link-control' - ); - expect( popover ).toBeNull(); - } ); - - it( 'allows Left to be pressed during creation in "Docked Toolbar" mode', async () => { - await toggleFixedToolbar( false ); - - await clickBlockAppender(); - await page.keyboard.type( 'Text' ); - - await clickBlockToolbarButton( 'Link' ); - - // Typing "left" should not close the dialog. - await page.keyboard.press( 'ArrowLeft' ); - let popover = await page.$( - '.components-popover__content .block-editor-link-control' - ); - expect( popover ).not.toBeNull(); - - // Escape should close the dialog still. - await page.keyboard.press( 'Escape' ); - popover = await page.$( - '.components-popover__content .block-editor-link-control' - ); - expect( popover ).toBeNull(); - } ); - - it( 'can be edited with collapsed selection', async () => { - await createAndReselectLink(); - // Make a collapsed selection inside the link - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - await showBlockToolbar(); - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - await editButton.click(); - await waitForURLFieldAutoFocus(); - await page.keyboard.type( '/handbook' ); - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - const createPostWithTitle = async ( titleText ) => { - await createNewPost(); - await canvas().type( '.editor-post-title__input', titleText ); - await page.click( '.editor-post-publish-panel__toggle' ); - - // Disable reason: Wait for the animation to complete, since otherwise the - // click attempt may occur at the wrong point. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 100 ); - - // Publish the post. - await page.click( '.editor-post-publish-button' ); - - // Return the URL of the new post. - await page.waitForSelector( - '.post-publish-panel__postpublish-post-address input' - ); - return page.evaluate( - () => - document.querySelector( - '.post-publish-panel__postpublish-post-address input' - ).value - ); - }; - - it( 'allows use of escape key to dismiss the url popover', async () => { - const titleText = 'Test post escape'; - await createPostWithTitle( titleText ); - - await createNewPost(); - await clickBlockAppender(); - - // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. - await page.keyboard.type( 'This is Gutenberg' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Trigger the autocomplete suggestion list and select the first suggestion. - await page.keyboard.type( titleText ); - await page.waitForSelector( '.block-editor-link-control__search-item' ); - await page.keyboard.press( 'ArrowDown' ); - - // Expect the escape key to dismiss the popover when the autocomplete suggestion list is open. - await page.keyboard.press( 'Escape' ); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - - // Confirm that selection is returned to where it was before launching - // the link editor, with "Gutenberg" as an uncollapsed selection. - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( '.' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Expect the escape key to dismiss the popover normally. - await page.keyboard.press( 'Escape' ); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Tab to the "Open in new tab" toggle. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Tab' ); - - // Expect the escape key to dismiss the popover normally. - await page.keyboard.press( 'Escape' ); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - } ); - - it( 'can be modified using the keyboard once a link has been set', async () => { - const URL = 'https://wordpress.org/gutenberg'; - - // Create a block with some text and format it as a link. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'K' ); - await waitForURLFieldAutoFocus(); - await page.keyboard.type( URL ); - await page.keyboard.press( 'Enter' ); - - // Deselect the link text by moving the caret to the end of the line - // and the link popover should not be displayed. - await page.keyboard.press( 'End' ); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - - // Move the caret back into the link text and the link popover - // should be displayed. - await page.keyboard.press( 'ArrowLeft' ); - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Press Cmd+K to edit the link and the url-input should become - // focused with the value previously inserted. - await pressKeyWithModifier( 'primary', 'K' ); - await waitForURLFieldAutoFocus(); - const isInURLInput = await page.evaluate( - () => !! document.activeElement.closest( '.block-editor-url-input' ) - ); - expect( isInURLInput ).toBe( true ); - const activeElementValue = await page.evaluate( - () => document.activeElement.value - ); - expect( activeElementValue ).toBe( URL ); - - // Confirm that submitting the input without any changes keeps the same - // value and moves focus back to the paragraph. - await page.keyboard.press( 'Enter' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( '.' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'adds an assertive message for screenreader users when an invalid link is set', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'K' ); - await waitForURLFieldAutoFocus(); - await page.keyboard.type( 'http://#test.com' ); - await page.keyboard.press( 'Enter' ); - const assertiveContent = await page.evaluate( - () => document.querySelector( '#a11y-speak-assertive' ).textContent - ); - expect( assertiveContent.trim() ).toBe( - 'Warning: the link has been inserted but may have errors. Please test it.' - ); - } ); - - describe( 'Editing link text', () => { - it( 'should not display text input when initially creating the link', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg: ' ); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - const [ settingsToggle ] = await page.$x( - '//button[contains(text(), "Advanced")]' - ); - await settingsToggle.click(); - - const textInput = await page - .waitForXPath( - '//[contains(@class, "block-editor-link-control__search-input-wrapper")]//label[contains(text(), "Text")]', - { - timeout: 1000, - } - ) - .catch( () => false ); - - expect( textInput ).toBeFalsy(); - } ); - - it( 'should display text input when the link has a valid URL value', async () => { - await createAndReselectLink(); - - // Make a collapsed selection inside the link. This is used - // as a stress test to ensure we can find the link text from a - // collapsed RichTextValue that contains a link format. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - await editButton.click(); - - await waitForURLFieldAutoFocus(); - - await pressKeyWithModifier( 'shift', 'Tab' ); - - // Tabbing should land us in the text input. - const { isTextInput, textValue } = await page.evaluate( () => { - const el = document.activeElement; - - return { - isTextInput: el.matches( 'input[type="text"]' ), - textValue: el.value, - }; - } ); - - // Let's check we've focused a text input. - expect( isTextInput ).toBe( true ); - - // Link was created on text value "Gutenberg". We expect - // the text input to reflect that value. - expect( textValue ).toBe( 'Gutenberg' ); - } ); - - it( 'should preserve trailing/leading whitespace from linked text in text input', async () => { - const textToSelect = ` spaces `; - const textWithWhitespace = `Text with leading and trailing${ textToSelect }`; - - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( textWithWhitespace ); - - // Use arrow keys to select only the text with the leading - // and trailing whitespace. - for ( let index = 0; index < textToSelect.length; index++ ) { - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - } - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Click on the Submit button. - await page.keyboard.press( 'Enter' ); - - // Reselect the link. - await page.keyboard.press( 'ArrowLeft' ); - - await showBlockToolbar(); - - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - - await editButton.click(); - - await waitForURLFieldAutoFocus(); - - // Tabbing backward should land us in the "Text" input. - await pressKeyWithModifier( 'shift', 'Tab' ); - - const textInputValue = await page.evaluate( - () => document.activeElement.value - ); - - expect( textInputValue ).toBe( textToSelect ); - } ); - - it( 'should allow for modification of link text via Link UI', async () => { - const originalLinkText = 'Gutenberg'; - const changedLinkText = - ' link text that was modified via the Link UI to include spaces '; - - await createAndReselectLink(); - - // Make a collapsed selection inside the link. This is used - // as a stress test to ensure we can find the link text from a - // collapsed RichTextValue that contains a link format. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - - await showBlockToolbar(); - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - await editButton.click(); - - await waitForURLFieldAutoFocus(); - - await pressKeyWithModifier( 'shift', 'Tab' ); - - const textInputValue = await page.evaluate( - () => document.activeElement.value - ); - - // At this point, we still expect the text input - // to reflect the original value with no modifications. - expect( textInputValue ).toBe( originalLinkText ); - - // Select all the link text in the input. - await pressKeyWithModifier( 'primary', 'a' ); - - // Modify the link text value. - await page.keyboard.type( changedLinkText ); - - // Submit the change. - await page.keyboard.press( 'Enter' ); - - // Check the created link reflects the link text. - const actualLinkText = await canvas().evaluate( - () => - document.querySelector( - '.block-editor-rich-text__editable a' - ).textContent - ); - expect( actualLinkText ).toBe( changedLinkText ); - } ); - - it( 'should display (capture the) text from the currently active link even if there is a rich text selection', async () => { - const originalLinkText = 'Gutenberg'; - - await createAndReselectLink(); - - // Make a collapsed selection inside the link in order - // to activate the Link UI. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - - const [ editButton ] = await page.$x( - '//button[contains(@aria-label, "Edit")]' - ); - await editButton.click(); - await waitForURLFieldAutoFocus(); - - const [ settingsToggle ] = await page.$x( - '//button[contains(text(), "Advanced")]' - ); - await settingsToggle.click(); - - // Wait for settings to open. - await page.waitForXPath( `//label[text()='Open in new tab']` ); - - // Move focus back to RichText for the underlying link. - await pressKeyWithModifier( 'shift', 'Tab' ); - await pressKeyWithModifier( 'shift', 'Tab' ); - await pressKeyWithModifier( 'shift', 'Tab' ); - - // Make a selection within the RichText. - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - - // Move back to the text input. - await pressKeyTimes( 'Tab', 1 ); - - // Tabbing back should land us in the text input. - const textInputValue = await page.evaluate( - () => document.activeElement.value - ); - - // Making a selection within the link text whilst the Link UI - // is open should not alter the value in the Link UI's text - // input. It should remain as the full text of the currently - // focused link format. - expect( textInputValue ).toBe( originalLinkText ); - } ); - } ); - - describe( 'Disabling Link UI active state', () => { - it( 'should not show the Link UI when selection extends beyond link boundary', async () => { - const linkedText = `Gutenberg`; - const textBeyondLinkedText = ` and more text.`; - - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( - `This is ${ linkedText }${ textBeyondLinkedText }` - ); - - // Move cursor next to end of `linkedText` - for ( - let index = 0; - index < textBeyondLinkedText.length; - index++ - ) { - await page.keyboard.press( 'ArrowLeft' ); - } - - // Select the linkedText. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Update the link. - await page.keyboard.press( 'Enter' ); - - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Make selection starting within the link and moving beyond boundary to the left. - for ( let index = 0; index < linkedText.length; index++ ) { - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - } - - // The Link UI should have disappeared (i.e. be inactive). - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - - // Cancel selection and move back within the Link. - await page.keyboard.press( 'ArrowRight' ); - - // We should see the Link UI displayed again. - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Make selection starting within the link and moving beyond boundary to the right. - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - - // The Link UI should have disappeared (i.e. be inactive). - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - } ); - - it( 'should not show the Link UI when selection extends into another link', async () => { - const linkedTextOne = `Gutenberg`; - const linkedTextTwo = `Block Editor`; - const linkOneURL = 'https://wordpress.org'; - const linkTwoURL = 'https://wordpress.org/gutenberg'; - - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( - `This is the ${ linkedTextOne }${ linkedTextTwo }` - ); - - // Select the linkedTextTwo. - for ( let index = 0; index < linkedTextTwo.length; index++ ) { - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - } - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( linkTwoURL ); - - // Update the link. - await page.keyboard.press( 'Enter' ); - - // Move cursor next to the **end** of `linkTextOne` - for ( let index = 0; index < linkedTextTwo.length + 2; index++ ) { - await page.keyboard.press( 'ArrowLeft' ); - } - - // Select `linkTextOne` - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( linkOneURL ); - - // Update the link. - await page.keyboard.press( 'Enter' ); - - // Move cursor within `linkTextOne` - for ( let index = 0; index < 3; index++ ) { - await page.keyboard.press( 'ArrowLeft' ); - } - - // Link UI should activate for `linkTextOne` - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).not.toBeNull(); - - // Expand selection so that it overlaps with `linkTextTwo` - for ( let index = 0; index < 3; index++ ) { - await pressKeyWithModifier( 'shift', 'ArrowRight' ); - } - - // Link UI should be inactive. - expect( - await page.$( - '.components-popover__content .block-editor-link-control' - ) - ).toBeNull(); - } ); - - // Based on issue reported in https://github.com/WordPress/gutenberg/issues/41771/. - it( 'should correctly replace targetted links text within rich text value when multiple matching values exist', async () => { - // Create a block with some text. - await clickBlockAppender(); - - // Note the two instances of the string "a". - await page.keyboard.type( `a b c a` ); - - // Select the last "a" only. - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - - // Click on the Link button. - await page.click( 'button[aria-label="Link"]' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'www.wordpress.org' ); - - // Update the link. - await page.keyboard.press( 'Enter' ); - - await page.keyboard.press( 'ArrowLeft' ); - - // Move to "Edit" button in Link UI - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - - await waitForURLFieldAutoFocus(); - - // Move to "Text" field. - await pressKeyWithModifier( 'shift', 'Tab' ); - - // Delete existing value from "Text" field - await page.keyboard.press( 'Delete' ); - - // Change text to "z" - await page.keyboard.type( 'z' ); - - await page.keyboard.press( 'Enter' ); - - const richTextText = await canvas().evaluate( - () => - document.querySelector( - '.block-editor-rich-text__editable' - ).textContent - ); - // Check that the correct (i.e. last) instance of "a" was replaced with "z". - expect( richTextText ).toBe( 'a b c z' ); - - const richTextLink = await canvas().evaluate( - () => - document.querySelector( - '.block-editor-rich-text__editable a' - ).textContent - ); - // Check that the correct (i.e. last) instance of "a" was replaced with "z". - expect( richTextLink ).toBe( 'z' ); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js index 55d126314d1fc6..7e654ca12790f7 100644 --- a/test/e2e/specs/editor/blocks/links.spec.js +++ b/test/e2e/specs/editor/blocks/links.spec.js @@ -8,10 +8,525 @@ test.describe( 'Links', () => { await admin.createNewPost(); } ); + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test.use( { + LinkUtils: async ( { editor, page, pageUtils }, use ) => { + await use( new LinkUtils( { editor, page, pageUtils } ) ); + }, + } ); + + test( `will use Post title as link text if link to existing post is created without any text selected`, async ( { + admin, + page, + editor, + requestUtils, + } ) => { + const titleText = 'Post to create a link to'; + const { id: postId } = await requestUtils.createPost( { + title: titleText, + status: 'publish', + } ); + + await admin.createNewPost(); + + // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'Here comes a link: ' ); + + // Insert a link deliberately not selecting any text. + await editor.clickBlockToolbarButton( 'Link' ); + + // Trigger the autocomplete suggestion list and select the first suggestion. + await page.keyboard.type( 'Post to create a' ); + await page.getByRole( 'option', { name: titleText } ).click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'Here comes a link: ' + + titleText + + '', + }, + }, + ] ); + } ); + + test( `can be created by selecting text and clicking link insertion button in block toolbar`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg' ); + + // Select some text. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button in the Block Toolbar. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Submit the link. + await pageUtils.pressKeys( 'Enter' ); + + // The link should have been inserted. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is Gutenberg', + }, + }, + ] ); + } ); + + test( `will not automatically create a link if selected text is not a valid HTTP based URL`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This: is not a link' ); + + // Select some text. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + await expect( + page.getByRole( 'combobox', { + name: 'Link', + } ) + ).toHaveValue( '' ); + } ); + + test( `can be created without any text selected`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg: ' ); + + // Press Cmd+K to insert a link. + await pageUtils.pressKeys( 'primary+K' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Press Enter to apply the link. + await pageUtils.pressKeys( 'Enter' ); + + // A link with the URL as its text should have been inserted. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is Gutenberg: https://wordpress.org/gutenberg', + }, + }, + ] ); + } ); + + test( `will automatically create a link if selected text is a valid HTTP based URL`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( + 'This is Gutenberg: https://wordpress.org/gutenberg' + ); + + // Select the URL. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft', { times: 7 } ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // A link with the selected URL as its href should have been inserted. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is Gutenberg: https://wordpress.org/gutenberg', + }, + }, + ] ); + } ); + + test( `does not create link when link ui is closed without submission`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg' ); + + // Select some text. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Click somewhere else - it doesn't really matter where. + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .focus(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is Gutenberg', + }, + }, + ] ); + } ); + + test( `can edit existing links`, async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + await LinkUtils.createAndReselectLink(); + + // Click on the Edit button. + await page.getByRole( 'button', { name: 'Edit' } ).click(); + + // Change the URL. + // getByPlaceholder required in order to handle Link Control component + // managing focus onto other inputs within the control. + await page.getByPlaceholder( 'Search or type url' ).fill( '' ); + await page.keyboard.type( '/handbook' ); + + // Submit the link. + await pageUtils.pressKeys( 'Enter' ); + + // The link should have been updated. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is Gutenberg', + }, + }, + ] ); + } ); + + test( `can remove existing links`, async ( { editor, LinkUtils } ) => { + await LinkUtils.createAndReselectLink(); + + const linkPopover = LinkUtils.getLinkPopover(); + + await linkPopover.getByRole( 'button', { name: 'Unlink' } ).click(); + + // The link should have been removed. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is Gutenberg', + }, + }, + ] ); + } ); + + test( `allows arrow keys to be pressed during link creation when the toolbar is fixed to top`, async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + await LinkUtils.toggleFixedToolbar( true ); + + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'Text' ); + await editor.clickBlockToolbarButton( 'Link' ); + + const linkPopover = LinkUtils.getLinkPopover(); + await expect( linkPopover ).toBeVisible(); + + // Pressing "left" should not close the dialog. + await pageUtils.pressKeys( 'ArrowLeft' ); + await expect( linkPopover ).toBeVisible(); + + // Escape should close the dialog. + await page.keyboard.press( 'Escape' ); + + await expect( linkPopover ).toBeHidden(); + } ); + + test( `allows arrow keys to be pressed during link creation in "Docked Toolbar" mode`, async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + await LinkUtils.toggleFixedToolbar( false ); + + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'Text' ); + + await editor.clickBlockToolbarButton( 'Link' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + await expect( linkPopover ).toBeVisible(); + + // Pressing arrow key should not close the dialog. + await pageUtils.pressKeys( 'ArrowLeft' ); + await expect( linkPopover ).toBeVisible(); + + // Escape should close the dialog still. + await page.keyboard.press( 'Escape' ); + + await expect( linkPopover ).toBeHidden(); + } ); + + test( `can be edited when within a link but no selection has been made ("collapsed")`, async ( { + page, + editor, + LinkUtils, + pageUtils, + } ) => { + await LinkUtils.createAndReselectLink(); + // Make a collapsed selection inside the link. + await pageUtils.pressKeys( 'ArrowLeft' ); + await pageUtils.pressKeys( 'ArrowRight' ); + + const linkPopover = LinkUtils.getLinkPopover(); + await linkPopover.getByRole( 'button', { name: 'Edit' } ).click(); + + // Change the URL. + // getByPlaceholder required in order to handle Link Control component + // managing focus onto other inputs within the control. + await page.getByPlaceholder( 'Search or type url' ).fill( '' ); + await page.keyboard.type( '/handbook' ); + + // Submit the link. + await pageUtils.pressKeys( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is Gutenberg', + }, + }, + ] ); + } ); + + test( `escape dismisses the Link UI popover and returns focus`, async ( { + admin, + page, + editor, + pageUtils, + requestUtils, + LinkUtils, + } ) => { + const titleText = 'Test post escape'; + await requestUtils.createPost( { + title: titleText, + status: 'publish', + } ); + + await admin.createNewPost(); + + // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + + await page.keyboard.type( 'This is Gutenberg' ); + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Insert a link. + await editor.clickBlockToolbarButton( 'Link' ); + + const urlInput = page.getByRole( 'combobox', { + name: 'Link', + } ); + + // Expect the "Link" combobox to be visible and focused + await expect( urlInput ).toBeVisible(); + await expect( urlInput ).toBeFocused(); + + // Trigger the autocomplete suggestion list. + await page.keyboard.type( titleText ); + await expect( + page.getByRole( 'option', { + // "post" disambiguates from the "Create page" option. + name: `${ titleText } post`, + } ) + ).toBeVisible(); + + // Move into the suggestions list. + await page.keyboard.press( 'ArrowDown' ); + + // Expect the escape key to dismiss the popover when the autocomplete suggestion list is open. + // Note that these have their own keybindings thus why we need to assert on this behaviour. + await page.keyboard.press( 'Escape' ); + await expect( LinkUtils.getLinkPopover() ).toBeHidden(); + + // Confirm that selection is returned to where it was before launching + // the link editor, with "Gutenberg" as an uncollapsed selection. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ' and more!' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is Gutenberg and more!', + }, + }, + ] ); + } ); + + test( `can be created and modified using only the keyboard`, async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + const URL = 'https://wordpress.org/gutenberg'; + + // Create a block with some text and format it as a link. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg' ); + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + await pageUtils.pressKeys( 'primary+K' ); + await page.keyboard.type( URL ); + await pageUtils.pressKeys( 'Enter' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + // Deselect the link text by moving the caret to the end of the line + // and the link popover should not be displayed. + await pageUtils.pressKeys( 'End' ); + await expect( linkPopover ).toBeHidden(); + + // Move the caret back into the link text and the link popover + // should be displayed. + await pageUtils.pressKeys( 'ArrowLeft' ); + await expect( linkPopover ).toBeVisible(); + + // Switch the Link UI into "Edit" mode via keyboard shortcut + // and check that the input has the correct value. + await pageUtils.pressKeys( 'primary+K' ); + + await expect( + linkPopover.getByRole( 'combobox', { + name: 'Link', + } ) + ).toHaveValue( URL ); + + // Confirm that submitting the input without any changes keeps the same + // value and moves focus back to the paragraph. + + // Submit without changes - should return to preview mode. + await pageUtils.pressKeys( 'Enter' ); + + // Move back into the RichText. + await pageUtils.pressKeys( 'Escape' ); + + // ...but the Link Popover should still be active because we are within the link. + await expect( linkPopover ).toBeVisible(); + + // Move outside of the link entirely. + await pageUtils.pressKeys( 'ArrowRight' ); + + // Link Popover should now disappear because we are no longer within the link. + await expect( linkPopover ).toBeHidden(); + + // Append some text to the paragraph to assert that focus has been returned + // to the correct location within the RichText. + await page.keyboard.type( ' and more!' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is Gutenberg and more!', + }, + }, + ] ); + } ); + + test( `adds an assertive message for screenreader users when an invalid link is set`, async ( { + page, + editor, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg' ); + // Select some text. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Insert a Link. + await editor.clickBlockToolbarButton( 'Link' ); + + await page.keyboard.type( 'http://#test.com' ); + await pageUtils.pressKeys( 'Enter' ); + expect( + page.getByText( + 'Warning: the link has been inserted but may have errors. Please test it.' + ) + ).toBeTruthy(); + } ); + test( `can be created by selecting text and using keyboard shortcuts`, async ( { page, editor, pageUtils, + LinkUtils, } ) => { // Create a block with some text. await editor.insertBlock( { @@ -63,12 +578,10 @@ test.describe( 'Links', () => { await expect( checkbox ).toBeChecked(); await expect( checkbox ).toBeFocused(); + const linkPopover = LinkUtils.getLinkPopover(); + // Tab back to the Submit and apply the link. - await page - //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved. - .locator( '.block-editor-link-control' ) - .getByRole( 'button', { name: 'Save' } ) - .click(); + await linkPopover.getByRole( 'button', { name: 'Save' } ).click(); // The link should have been inserted. await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -86,6 +599,7 @@ test.describe( 'Links', () => { page, editor, pageUtils, + LinkUtils, } ) => { // Create a block with some text. await editor.insertBlock( { @@ -114,15 +628,16 @@ test.describe( 'Links', () => { // Edit link. await pageUtils.pressKeys( 'primary+k' ); + + // getByPlaceholder required in order to handle Link Control component + // managing focus onto other inputs within the control. await page.getByPlaceholder( 'Search or type url' ).fill( '' ); await page.keyboard.type( 'wordpress.org' ); + const linkPopover = LinkUtils.getLinkPopover(); + // Update the link. - await page - //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved. - .locator( '.block-editor-link-control' ) - .getByRole( 'button', { name: 'Save' } ) - .click(); + await linkPopover.getByRole( 'button', { name: 'Save' } ).click(); // Navigate back to the popover. await page.keyboard.press( 'ArrowLeft' ); @@ -130,10 +645,14 @@ test.describe( 'Links', () => { // Navigate back to inputs to verify appears as changed. await pageUtils.pressKeys( 'primary+k' ); - const urlInputValue = await page - .getByPlaceholder( 'Search or type url' ) - .inputValue(); - expect( urlInputValue ).toContain( 'wordpress.org' ); + + expect( + await page + .getByRole( 'combobox', { + name: 'Link', + } ) + .inputValue() + ).toContain( 'wordpress.org' ); await expect.poll( editor.getBlocks ).toMatchObject( [ { @@ -238,6 +757,7 @@ test.describe( 'Links', () => { page, editor, pageUtils, + LinkUtils, } ) => { await editor.insertBlock( { name: 'core/paragraph', @@ -271,11 +791,10 @@ test.describe( 'Links', () => { await page.getByLabel( 'Open in new tab' ).click(); await page.getByLabel( 'nofollow' ).click(); + const linkPopover = LinkUtils.getLinkPopover(); + // Save the link - await page - .locator( '.block-editor-link-control' ) - .getByRole( 'button', { name: 'Save' } ) - .click(); + await linkPopover.getByRole( 'button', { name: 'Save' } ).click(); // Expect correct attributes to be set on the underlying link. await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -300,10 +819,7 @@ test.describe( 'Links', () => { await page.getByLabel( 'nofollow' ).click(); // Save the link - await page - .locator( '.block-editor-link-control' ) - .getByRole( 'button', { name: 'Save' } ) - .click(); + await linkPopover.getByRole( 'button', { name: 'Save' } ).click(); // Expect correct attributes to be set on the underlying link. await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -315,4 +831,445 @@ test.describe( 'Links', () => { }, ] ); } ); + + test.describe( 'Editing link text', () => { + test( 'should allow for modification of link text via the Link UI', async ( { + page, + pageUtils, + editor, + LinkUtils, + } ) => { + await LinkUtils.createAndReselectLink(); + + const originalLinkText = 'Gutenberg'; + const changedLinkText = + ' link text that was modified via the Link UI to include spaces '; + + // Make a collapsed selection inside the link. This is used + // as a stress test to ensure we can find the link text from a + // collapsed RichTextValue that contains a link format. + await pageUtils.pressKeys( 'ArrowLeft' ); + await pageUtils.pressKeys( 'ArrowRight' ); + + await editor.showBlockToolbar(); + await page.getByRole( 'button', { name: 'Edit' } ).click(); + + const textInput = page.getByLabel( 'Text', { exact: true } ); + + // At this point, we still expect the text input + // to reflect the original value with no modifications. + await expect( textInput ).toHaveValue( originalLinkText ); + + // Select all the link text in the input. + await pageUtils.pressKeys( 'primary+a' ); + + // Modify the link text value. + await page.keyboard.type( changedLinkText ); + + // Submit the change. + await pageUtils.pressKeys( 'Enter' ); + + // Check the created link reflects the link text. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is ' + + changedLinkText + + '', + }, + }, + ] ); + } ); + + test( 'should not display text input when initially creating the link', async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg: ' ); + + // Press Cmd+K to insert a link. + await pageUtils.pressKeys( 'primary+k' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + // Check the Link UI is open before asserting on presence of text input + // within that control. + await expect( linkPopover ).toBeVisible(); + + // Let's check we've focused a text input. + const textInput = linkPopover.getByLabel( 'Text', { exact: true } ); + await expect( textInput ).toBeHidden(); + } ); + + test( 'should display text input when the link has a valid URL value', async ( { + pageUtils, + LinkUtils, + } ) => { + await LinkUtils.createAndReselectLink(); + + // Make a collapsed selection inside the link. This is used + // as a stress test to ensure we can find the link text from a + // collapsed RichTextValue that contains a link format. + await pageUtils.pressKeys( 'ArrowLeft' ); + await pageUtils.pressKeys( 'ArrowRight' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + await linkPopover.getByRole( 'button', { name: 'Edit' } ).click(); + + // Check Text input is visible and is the focused field. + const textInput = linkPopover.getByLabel( 'Text', { exact: true } ); + await expect( textInput ).toBeVisible(); + await expect( textInput ).toBeFocused(); + + // Link was created on text value "Gutenberg". We expect + // the text input to reflect that value. + await expect( textInput ).toHaveValue( 'Gutenberg' ); + } ); + + test( 'should show any trailing and/or leading whitespace from linked text within the text input', async ( { + page, + pageUtils, + editor, + } ) => { + const textToSelect = ` spaces `; + const textWithWhitespace = `Text with leading and trailing${ textToSelect }`; + + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( textWithWhitespace ); + + // Use arrow keys to select only the text with the leading + // and trailing whitespace. + await pageUtils.pressKeys( 'shift+ArrowLeft', { + times: textToSelect.length, + } ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Click on the Submit button. + await pageUtils.pressKeys( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'Text with leading and trailing' + + textToSelect + + '', + }, + }, + ] ); + } ); + + test( 'should display (capture the) text from the currently active link even if there is a rich text selection', async ( { + editor, + pageUtils, + LinkUtils, + } ) => { + const originalLinkText = 'Gutenberg'; + + await LinkUtils.createAndReselectLink(); + + // Make a collapsed selection inside the link in order + // to activate the Link UI. + await pageUtils.pressKeys( 'ArrowLeft' ); + await pageUtils.pressKeys( 'ArrowRight' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + await linkPopover.getByRole( 'button', { name: 'Edit' } ).click(); + + // Place cursor within the underling RichText link (not the Link UI). + await editor.canvas + .getByRole( 'document', { + name: 'Block: Paragraph', + } ) + .getByRole( 'link', { + name: 'Gutenberg', + } ) + .click(); + + // Make a selection within the RichText. + await pageUtils.pressKeys( 'shift+ArrowRight', { + times: 3, + } ); + + // Making a selection within the link text whilst the Link UI + // is open should not alter the value in the Link UI's "Text" + // field. It should remain as the full text of the currently + // focused link format. + await expect( + linkPopover.getByLabel( 'Text', { exact: true } ) + ).toHaveValue( originalLinkText ); + } ); + } ); + + test.describe( 'Disabling Link UI active state', () => { + test( 'should not show the Link UI when selection extends beyond link boundary', async ( { + page, + pageUtils, + editor, + LinkUtils, + } ) => { + const linkedText = `Gutenberg`; + const textBeyondLinkedText = ` and more text.`; + + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( + `This is ${ linkedText }${ textBeyondLinkedText }` + ); + + // Move cursor next to end of `linkedText`. + await pageUtils.pressKeys( 'ArrowLeft', { + times: textBeyondLinkedText.length, + } ); + + // Select the linkedText. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Update the link. + await pageUtils.pressKeys( 'Enter' ); + // Reactivate the link. + await pageUtils.pressKeys( 'ArrowLeft' ); + await pageUtils.pressKeys( 'ArrowLeft' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + await expect( linkPopover ).toBeVisible(); + + // Make selection starting within the link and moving beyond boundary to the left. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft', { + times: linkedText.length, + } ); + + // The Link UI should have disappeared (i.e. be inactive). + await expect( linkPopover ).toBeHidden(); + + // Cancel selection and move back within the Link. + await pageUtils.pressKeys( 'ArrowRight' ); + + // We should see the Link UI displayed again. + await expect( linkPopover ).toBeVisible(); + + // Make selection starting within the link and moving beyond boundary to the right. + await pageUtils.pressKeys( 'shift+ArrowRight', { + times: 3, + } ); + + // The Link UI should have disappeared (i.e. be inactive). + await expect( linkPopover ).toBeHidden(); + } ); + + test( 'should not show the Link UI when selection extends into another link', async ( { + page, + pageUtils, + editor, + LinkUtils, + } ) => { + const linkedTextOne = `Gutenberg`; + const linkedTextTwo = `Block Editor`; + const linkOneURL = 'https://wordpress.org'; + const linkTwoURL = 'https://wordpress.org/gutenberg'; + + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( + `This is the ${ linkedTextOne }${ linkedTextTwo }` + ); + + // Select the linkedTextTwo. + await pageUtils.pressKeys( 'shift+ArrowLeft', { + times: linkedTextTwo.length, + } ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( linkTwoURL ); + + // Update the link. + await pageUtils.pressKeys( 'Enter' ); + + // Move cursor next to the **end** of `linkTextOne` + await pageUtils.pressKeys( 'ArrowLeft', { + times: linkedTextTwo.length, + } ); + + // Select `linkTextOne` + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( linkOneURL ); + + // Update the link. + await pageUtils.pressKeys( 'Enter' ); + + // Move cursor within `linkTextOne` + await pageUtils.pressKeys( 'ArrowLeft', { + times: 3, + } ); + + const linkPopover = LinkUtils.getLinkPopover(); + + // Link UI should activate for `linkTextOne` + await expect( linkPopover ).toBeVisible(); + + // Expand selection so that it overlaps with `linkTextTwo` + await pageUtils.pressKeys( 'ArrowRight', { + times: 3, + } ); + + // Link UI should be inactive. + await expect( linkPopover ).toBeHidden(); + } ); + + // Based on issue reported in https://github.com/WordPress/gutenberg/issues/41771/. + test( `should correctly replace active link's text value within rich text even when multiple matching text values exist within the rich text`, async ( { + page, + editor, + pageUtils, + LinkUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + + // Note the two instances of the string "a". + await page.keyboard.type( `a b c a` ); + + // Select the last "a" only. + await pageUtils.pressKeys( 'shift+ArrowLeft' ); + + // Click on the Link button. + await editor.clickBlockToolbarButton( 'Link' ); + + // Type a URL. + await page.keyboard.type( 'www.wordpress.org' ); + + // Update the link. + await pageUtils.pressKeys( 'Enter' ); + + await pageUtils.pressKeys( 'ArrowLeft' ); + + const linkPopover = LinkUtils.getLinkPopover(); + + // Click the "Edit" button in Link UI + await linkPopover.getByRole( 'button', { name: 'Edit' } ).click(); + + // Focus the "Text" field within the linkPopover + await linkPopover + .getByRole( 'textbox', { + name: 'Text', + } ) + .focus(); + + // Delete existing value from "Text" field + await pageUtils.pressKeys( 'Backspace' ); + + // Change text to "z" + await page.keyboard.type( 'z' ); + + await pageUtils.pressKeys( 'Enter' ); + + // Check that the correct (i.e. last) instance of "a" was replaced with "z". + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'a b c z', + }, + }, + ] ); + } ); + } ); } ); + +class LinkUtils { + constructor( { editor, page, pageUtils } ) { + this.page = page; + this.editor = editor; + this.pageUtils = pageUtils; + } + + async toggleFixedToolbar( isFixed ) { + await this.page.evaluate( ( _isFixed ) => { + const { select, dispatch } = window.wp.data; + const isCurrentlyFixed = + select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); + + if ( isCurrentlyFixed !== _isFixed ) { + dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); + } + }, isFixed ); + } + + async createAndReselectLink() { + // Create a block with some text. + await this.editor.insertBlock( { + name: 'core/paragraph', + } ); + await this.page.keyboard.type( 'This is Gutenberg' ); + + // Select some text. + await this.pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Click on the Link button. + await this.page.getByRole( 'button', { name: 'Link' } ).click(); + + // Type a URL. + await this.page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Click on the Submit button. + await this.pageUtils.pressKeys( 'Enter' ); + + // Reselect the link. + await this.pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + } + + /** + * This method is used as a temporary workaround for retriveing the + * LinkControl component. This is because it currently does not expose + * any accessible attributes. In general we should avoid using this method + * and instead rely on locating the sub elements of the component directly. + * Remove / update method once the following PR has landed: + * https://github.com/WordPress/gutenberg/pull/54063. + */ + getLinkPopover() { + return this.page.locator( + '.components-popover__content .block-editor-link-control' + ); + } +}