diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index 5a002f5324c4e..a42594f3f2dce 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -317,3 +317,462 @@ test.describe( 'Navigation block', () => { } ); } ); } ); + +test.describe( 'List view editing', () => { + const navMenuBlocksFixture = { + title: 'Test Menu', + content: ` + + + `, + }; + + test.use( { + linkControl: async ( { page }, use ) => { + await use( new LinkControl( { page } ) ); + }, + } ); + + test( 'show a list view in the inspector controls', async ( { + admin, + page, + editor, + requestUtils, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + await expect( + page.getByRole( 'tab', { + name: 'List View', + } ) + ).toBeVisible(); + + const listViewPanel = page.getByRole( 'tabpanel', { + name: 'List View', + } ); + + await expect( listViewPanel ).toBeVisible(); + + await expect( + listViewPanel.getByRole( 'heading', { + name: 'Menu', + } ) + ).toBeVisible(); + + await expect( + listViewPanel.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ) + ).toBeVisible(); + } ); + + test( `list view should correctly reflect navigation items' structure`, async ( { + admin, + page, + editor, + requestUtils, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + // Check the structure of the individual menu items matches the one that was created. + await expect( + listView + .getByRole( 'gridcell', { + name: 'Page Link link', + } ) + .filter( { + hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. + } ) + .getByText( 'Top Level Item 1' ) + ).toBeVisible(); + + await expect( + listView + .getByRole( 'gridcell', { + name: 'Submenu link', + } ) + .filter( { + hasText: 'Block 2 of 2, Level 1', // proxy for filtering by description. + } ) + .getByText( 'Top Level Item 2' ) + ).toBeVisible(); + + await expect( + listView + .getByRole( 'gridcell', { + name: 'Page Link link', + } ) + .filter( { + hasText: 'Block 1 of 1, Level 2', // proxy for filtering by description. + } ) + .getByText( 'Test Submenu Item' ) + ).toBeVisible(); + } ); + + test( `can add new menu items`, async ( { + admin, + page, + editor, + requestUtils, + linkControl, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + const appender = listView.getByRole( 'button', { + name: 'Add block', + } ); + + await expect( appender ).toBeVisible(); + + await appender.click(); + + // Expect to see the block inserter. + await expect( + page.getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + ).toBeFocused(); + + const blockResults = page.getByRole( 'listbox', { + name: 'Blocks', + } ); + + await expect( blockResults ).toBeVisible(); + + const blockResultOptions = blockResults.getByRole( 'option' ); + + // Expect to see the Page Link and Custom Link blocks as the nth(0) and nth(1) results. + // This is important for usability as the Page Link block is the most likely to be used. + await expect( blockResultOptions.nth( 0 ) ).toHaveText( 'Page Link' ); + await expect( blockResultOptions.nth( 1 ) ).toHaveText( 'Custom Link' ); + + // Select the Page Link option. + const pageLinkResult = blockResultOptions.nth( 0 ); + await pageLinkResult.click(); + + // Expect to see the Link creation UI be focused. + const linkUIInput = linkControl.getSearchInput(); + + await expect( linkUIInput ).toBeFocused(); + + const firstResult = await linkControl.getNthSearchResult( 0 ); + + // Grab the text from the first result so we can check it was inserted. + const firstResultText = await linkControl.getSearchResultText( + firstResult + ); + + // Create the link. + await firstResult.click(); + + // Check the new menu item was inserted at the end of the existing menu. + await expect( + listView + .getByRole( 'gridcell', { + name: 'Page Link link', + } ) + .filter( { + hasText: 'Block 3 of 3, Level 1', // proxy for filtering by description. + } ) + .getByText( firstResultText ) + ).toBeVisible(); + } ); + + test( `can remove menu items`, async ( { + admin, + page, + editor, + requestUtils, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + const submenuOptions = listView.getByRole( 'button', { + name: 'Options for Submenu block', + } ); + + // Open the options menu. + await submenuOptions.click(); + + // usage of `page` is required because the options menu is rendered into a slot + // outside of the treegrid. + const removeBlockOption = page + .getByRole( 'menu', { + name: 'Options for Submenu block', + } ) + .getByRole( 'menuitem', { + name: 'Remove Top Level Item 2', + } ); + + await removeBlockOption.click(); + + // Check the menu item was removed. + await expect( + listView + .getByRole( 'gridcell', { + name: 'Submenu link', + } ) + .filter( { + hasText: 'Block 2 of 2, Level 1', // proxy for filtering by description. + } ) + .getByText( 'Top Level Item 2' ) + ).not.toBeVisible(); + } ); + + test( `can edit menu items`, async ( { + admin, + page, + editor, + requestUtils, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + // Click on the first menu item to open its settings. + const firstMenuItemAnchor = listView.getByRole( 'link', { + name: 'Top Level Item 1', + includeHidden: true, + } ); + await firstMenuItemAnchor.click(); + + // Get the settings panel. + const blockSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + await expect( blockSettings ).toBeVisible(); + + await expect( + blockSettings.getByRole( 'heading', { + name: 'Page Link', + } ) + ).toBeVisible(); + + await expect( + blockSettings.getByRole( 'tab', { + name: 'Settings', + selected: true, + } ) + ).toBeVisible(); + + await expect( + blockSettings + .getByRole( 'tabpanel', { + name: 'Settings', + } ) + .getByRole( 'heading', { + name: 'Link Settings', + } ) + ).toBeVisible(); + + const labelInput = blockSettings.getByRole( 'textbox', { + name: 'Label', + } ); + + await expect( labelInput ).toHaveValue( 'Top Level Item 1' ); + + await labelInput.focus(); + + await page.keyboard.type( 'Changed label' ); + + // Click the back button to go back to the Nav block. + await blockSettings + .getByRole( 'button', { + name: 'Go to parent Navigation block', + } ) + .click(); + + // Check we're back on the Nav block list view. + const listViewPanel = page.getByRole( 'tabpanel', { + name: 'List View', + } ); + + await expect( listViewPanel ).toBeVisible(); + + // Check the label was updated. + await expect( + listViewPanel + .getByRole( 'gridcell', { + name: 'Page Link link', + } ) + .filter( { + hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. + } ) + .getByText( 'Changed label' ) // new label text + ).toBeVisible(); + } ); + + test( `can add submenus`, async ( { + admin, + page, + editor, + requestUtils, + linkControl, + } ) => { + await admin.createNewPost(); + await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + + await editor.insertBlock( { name: 'core/navigation' } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + // click on options menu for the first menu item and select remove. + const firstMenuItem = listView + .getByRole( 'gridcell', { + name: 'Page Link link', + } ) + .filter( { + hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. + } ); + + // The options menu button is a sibling of the menu item gridcell. + const firstItemOptions = firstMenuItem + .locator( '..' ) // parent selector. + .getByRole( 'button', { + name: 'Options for Page Link block', + } ); + + // Open the options menu. + await firstItemOptions.click(); + + // Add the submenu. + // usage of `page` is required because the options menu is rendered into a slot + // outside of the treegrid. + const addSubmenuOption = page + .getByRole( 'menu', { + name: 'Options for Page Link block', + } ) + .getByRole( 'menuitem', { + name: 'Add submenu link', + } ); + + await addSubmenuOption.click(); + + await linkControl.searchFor( 'https://wordpress.org' ); + + await page.keyboard.press( 'Enter' ); + + // Check the new item was inserted in the correct place. + await expect( + listView + .getByRole( 'gridcell', { + name: 'Custom Link link', + } ) + .filter( { + hasText: 'Block 1 of 1, Level 2', // proxy for filtering by description. + } ) + .getByText( 'wordpress.org' ) + ).toBeVisible(); + + // Check that the original item is still there but that it is now + // a submenu item. + await expect( + listView + .getByRole( 'gridcell', { + name: 'Submenu link', + } ) + .filter( { + hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. + } ) + .getByText( 'Top Level Item 1' ) + ).toBeVisible(); + } ); +} ); + +class LinkControl { + constructor( { page } ) { + this.page = page; + } + + getSearchInput() { + return this.page.getByRole( 'combobox', { + name: 'URL', + } ); + } + + async getSearchResults() { + const searchInput = this.getSearchInput(); + + const resultsRef = await searchInput.getAttribute( 'aria-owns' ); + + const linkUIResults = this.page.locator( `#${ resultsRef }` ); + + await expect( linkUIResults ).toBeVisible(); + + return linkUIResults.getByRole( 'option' ); + } + + async getNthSearchResult( index = 0 ) { + const results = await this.getSearchResults(); + return results.nth( index ); + } + + async searchFor( searchTerm = 'https://wordpress.org' ) { + const input = this.getSearchInput(); + + await expect( input ).toBeFocused(); + + await this.page.keyboard.type( searchTerm ); + + await expect( input ).toHaveValue( searchTerm ); + + return input; + } + + async getSearchResultText( result ) { + await expect( result ).toBeVisible(); + + return result + .locator( '.block-editor-link-control__search-item-title' ) // this is the only way to get the label text without the URL. + .innerText(); + } +}