diff --git a/packages/block-editor/src/components/inserter/block-list.js b/packages/block-editor/src/components/inserter/block-list.js index df82126e68bfdc..077a0432acdbd0 100644 --- a/packages/block-editor/src/components/inserter/block-list.js +++ b/packages/block-editor/src/components/inserter/block-list.js @@ -52,7 +52,7 @@ const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ]; const MAX_SUGGESTED_ITEMS = 9; -function InserterBlockList( { +export function InserterBlockList( { rootClientId, onInsert, onHover, @@ -168,22 +168,13 @@ function InserterBlockList( { // Announce search results on change useEffect( () => { - const resultCount = Object.keys( itemsPerCategory ).reduce( - ( accumulator, currentCategorySlug ) => { - return ( - accumulator + itemsPerCategory[ currentCategorySlug ].length - ); - }, - 0 - ); - const resultsFoundMessage = sprintf( /* translators: %d: number of results. */ - _n( '%d result found.', '%d results found.', resultCount ), - resultCount + _n( '%d result found.', '%d results found.', filteredItems.length ), + filteredItems.length ); debouncedSpeak( resultsFoundMessage ); - }, [ itemsPerCategory, debouncedSpeak ] ); + }, [ filterValue, debouncedSpeak ] ); const hasItems = ! isEmpty( filteredItems ); const hasChildItems = childItems.length > 0; diff --git a/packages/block-editor/src/components/inserter/test/block-list.js b/packages/block-editor/src/components/inserter/test/block-list.js index c83df96520e099..bf27a966a0a621 100644 --- a/packages/block-editor/src/components/inserter/test/block-list.js +++ b/packages/block-editor/src/components/inserter/test/block-list.js @@ -1,54 +1,48 @@ /** * External dependencies */ -import TestUtils, { act } from 'react-dom/test-utils'; -import ReactDOM from 'react-dom'; +import { render, fireEvent } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import InserterBlockList from '../block-list'; -import useSelect from '../../../../../data/src/components/use-select'; +import { InserterBlockList as BaseInserterBlockList } from '../block-list'; import items, { categories, collections } from './fixtures'; -jest.mock( '../../../../../data/src/components/use-select', () => { - // This allows us to tweaak the returned value on each test +jest.mock( '@wordpress/data/src/components/use-select', () => { + // This allows us to tweak the returned value on each test const mock = jest.fn(); return mock; } ); -jest.mock( '../../../../../data/src/components/use-dispatch', () => { +jest.mock( '@wordpress/data/src/components/use-dispatch', () => { return { useDispatch: () => ( {} ), }; } ); -const getWrapperForProps = ( propOverrides ) => { - let wrapper; - act( () => { - wrapper = TestUtils.renderIntoDocument( - - ); - } ); +const debouncedSpeak = jest.fn(); - return wrapper; -}; - -const initializeMenuDefaultStateAndReturnElement = ( propOverrides ) => { - const wrapper = getWrapperForProps( propOverrides ); - // eslint-disable-next-line react/no-find-dom-node - return ReactDOM.findDOMNode( wrapper ); -}; +function InserterBlockList( props ) { + return ( + + ); +} -const initializeAllClosedMenuStateAndReturnElement = ( propOverrides ) => { - const element = initializeMenuDefaultStateAndReturnElement( propOverrides ); - const activeTabs = element.querySelectorAll( +const initializeAllClosedMenuState = ( propOverrides ) => { + const result = render( ); + const activeTabs = result.container.querySelectorAll( '.components-panel__body.is-opened button.components-panel__body-toggle' ); activeTabs.forEach( ( tab ) => { - TestUtils.Simulate.click( tab ); + fireEvent.click( tab ); } ); - return element; + return result; }; const assertNoResultsMessageToBePresent = ( element ) => { @@ -67,6 +61,8 @@ const assertNoResultsMessageNotToBePresent = ( element ) => { describe( 'InserterMenu', () => { beforeEach( () => { + debouncedSpeak.mockClear(); + useSelect.mockImplementation( () => ( { categories, collections, @@ -81,21 +77,21 @@ describe( 'InserterMenu', () => { collections, items: noItems, } ) ); - const element = initializeMenuDefaultStateAndReturnElement( { - filterValue: 'random', - } ); - const visibleBlocks = element.querySelector( + const { container } = render( + + ); + const visibleBlocks = container.querySelector( '.block-editor-block-types-list__item' ); expect( visibleBlocks ).toBe( null ); - assertNoResultsMessageToBePresent( element ); + assertNoResultsMessageToBePresent( container ); } ); it( 'should show only high utility items in the suggested tab', () => { - const element = initializeMenuDefaultStateAndReturnElement(); - const firstPanel = element.querySelector( + const { container } = render( ); + const firstPanel = container.querySelector( '.block-editor-inserter__panel-content' ); const visibleBlocks = firstPanel.querySelectorAll( @@ -108,11 +104,11 @@ describe( 'InserterMenu', () => { } ); it( 'should show items from the embed category in the embed tab', () => { - const element = initializeAllClosedMenuStateAndReturnElement(); - const embedTabContent = element.querySelectorAll( + const { container } = initializeAllClosedMenuState(); + const embedTabContent = container.querySelectorAll( '.block-editor-inserter__panel-content' )[ 4 ]; - const embedTabTitle = element.querySelectorAll( + const embedTabTitle = container.querySelectorAll( '.block-editor-inserter__panel-title' )[ 4 ]; const blocks = embedTabContent.querySelectorAll( @@ -124,15 +120,15 @@ describe( 'InserterMenu', () => { expect( blocks[ 0 ].textContent ).toBe( 'YouTube' ); expect( blocks[ 1 ].textContent ).toBe( 'A Text Embed' ); - assertNoResultsMessageNotToBePresent( element ); + assertNoResultsMessageNotToBePresent( container ); } ); it( 'should show reusable items in the reusable tab', () => { - const element = initializeAllClosedMenuStateAndReturnElement(); - const reusableTabContent = element.querySelectorAll( + const { container } = initializeAllClosedMenuState(); + const reusableTabContent = container.querySelectorAll( '.block-editor-inserter__panel-content' )[ 6 ]; - const reusableTabTitle = element.querySelectorAll( + const reusableTabTitle = container.querySelectorAll( '.block-editor-inserter__panel-title' )[ 6 ]; const blocks = reusableTabContent.querySelectorAll( @@ -143,15 +139,15 @@ describe( 'InserterMenu', () => { expect( blocks ).toHaveLength( 1 ); expect( blocks[ 0 ].textContent ).toBe( 'My reusable block' ); - assertNoResultsMessageNotToBePresent( element ); + assertNoResultsMessageNotToBePresent( container ); } ); it( 'should show the common category blocks', () => { - const element = initializeAllClosedMenuStateAndReturnElement(); - const commonTabContent = element.querySelectorAll( + const { container } = initializeAllClosedMenuState(); + const commonTabContent = container.querySelectorAll( '.block-editor-inserter__panel-content' )[ 1 ]; - const commonTabTitle = element.querySelectorAll( + const commonTabTitle = container.querySelectorAll( '.block-editor-inserter__panel-title' )[ 1 ]; const blocks = commonTabContent.querySelectorAll( @@ -164,12 +160,12 @@ describe( 'InserterMenu', () => { expect( blocks[ 1 ].textContent ).toBe( 'Advanced Text' ); expect( blocks[ 2 ].textContent ).toBe( 'Some Other Block' ); - assertNoResultsMessageNotToBePresent( element ); + assertNoResultsMessageNotToBePresent( container ); } ); it( 'should disable items with `isDisabled`', () => { - const element = initializeAllClosedMenuStateAndReturnElement(); - const layoutTabContent = element.querySelectorAll( + const { container } = initializeAllClosedMenuState(); + const layoutTabContent = container.querySelectorAll( '.block-editor-inserter__panel-content' )[ 2 ]; const disabledBlocks = layoutTabContent.querySelectorAll( @@ -181,36 +177,90 @@ describe( 'InserterMenu', () => { } ); it( 'should allow searching for items', () => { - const element = initializeMenuDefaultStateAndReturnElement( { - filterValue: 'text', - } ); + const { container } = render( + + ); - const matchingCategories = element.querySelectorAll( + const matchingCategories = container.querySelectorAll( '.block-editor-inserter__panel-title' ); expect( matchingCategories ).toHaveLength( 3 ); expect( matchingCategories[ 0 ].textContent ).toBe( 'Common blocks' ); expect( matchingCategories[ 1 ].textContent ).toBe( 'Embeds' ); + expect( matchingCategories[ 2 ].textContent ).toBe( 'Core' ); // "Core" namespace collection - const blocks = element.querySelectorAll( + const blocks = container.querySelectorAll( '.block-editor-block-types-list__item-title' ); + // There are five buttons present for 3 total distinct results. The + // additional two account for the collection results (repeated). expect( blocks ).toHaveLength( 5 ); + expect( debouncedSpeak ).toHaveBeenCalledWith( '3 results found.' ); + + // Default block results. expect( blocks[ 0 ].textContent ).toBe( 'Text' ); expect( blocks[ 1 ].textContent ).toBe( 'Advanced Text' ); expect( blocks[ 2 ].textContent ).toBe( 'A Text Embed' ); - assertNoResultsMessageNotToBePresent( element ); + // Collection results. + expect( blocks[ 3 ].textContent ).toBe( 'Text' ); + expect( blocks[ 4 ].textContent ).toBe( 'Advanced Text' ); + + assertNoResultsMessageNotToBePresent( container ); + } ); + + it( 'should allow searching for reusable blocks by title', () => { + const { container } = render( + + ); + + const matchingCategories = container.querySelectorAll( + '.block-editor-inserter__panel-title' + ); + + expect( matchingCategories ).toHaveLength( 2 ); + expect( matchingCategories[ 0 ].textContent ).toBe( 'Core' ); // "Core" namespace collection + expect( matchingCategories[ 1 ].textContent ).toBe( 'Reusable' ); + + const blocks = container.querySelectorAll( + '.block-editor-block-types-list__item-title' + ); + + // There are two buttons present for 1 total distinct result. The + // additional one accounts for the collection result (repeated). + expect( blocks ).toHaveLength( 2 ); + expect( debouncedSpeak ).toHaveBeenCalledWith( '1 result found.' ); + expect( blocks[ 0 ].textContent ).toBe( 'My reusable block' ); + expect( blocks[ 1 ].textContent ).toBe( 'My reusable block' ); + + assertNoResultsMessageNotToBePresent( container ); + } ); + + it( 'should speak after any change in search term', () => { + // The search result count should always be announced any time the user + // changes the search term, even if it results in the same count. + // + // See: https://github.com/WordPress/gutenberg/pull/22279#discussion_r423317161 + const { rerender } = render( + + ); + + rerender( ); + rerender( ); + + expect( debouncedSpeak ).toHaveBeenCalledTimes( 2 ); + expect( debouncedSpeak.mock.calls[ 0 ][ 0 ] ).toBe( '1 result found.' ); + expect( debouncedSpeak.mock.calls[ 1 ][ 0 ] ).toBe( '1 result found.' ); } ); it( 'should trim whitespace of search terms', () => { - const element = initializeMenuDefaultStateAndReturnElement( { - filterValue: ' text', - } ); + const { container } = render( + + ); - const matchingCategories = element.querySelectorAll( + const matchingCategories = container.querySelectorAll( '.block-editor-inserter__panel-title' ); @@ -218,7 +268,7 @@ describe( 'InserterMenu', () => { expect( matchingCategories[ 0 ].textContent ).toBe( 'Common blocks' ); expect( matchingCategories[ 1 ].textContent ).toBe( 'Embeds' ); - const blocks = element.querySelectorAll( + const blocks = container.querySelectorAll( '.block-editor-block-types-list__item-title' ); @@ -227,6 +277,6 @@ describe( 'InserterMenu', () => { expect( blocks[ 1 ].textContent ).toBe( 'Advanced Text' ); expect( blocks[ 2 ].textContent ).toBe( 'A Text Embed' ); - assertNoResultsMessageNotToBePresent( element ); + assertNoResultsMessageNotToBePresent( container ); } ); } );