diff --git a/editor/block-switcher/index.js b/editor/block-switcher/index.js index e563b6e40a748f..41c0ad3503bb2d 100644 --- a/editor/block-switcher/index.js +++ b/editor/block-switcher/index.js @@ -17,7 +17,8 @@ import './style.scss'; import { replaceBlocks } from '../actions'; import { getBlock } from '../selectors'; -class BlockSwitcher extends wp.element.Component { +// Only exported for testing. +export class BlockSwitcher extends wp.element.Component { constructor() { super( ...arguments ); this.toggleMenu = this.toggleMenu.bind( this ); diff --git a/editor/block-switcher/test/index.js b/editor/block-switcher/test/index.js new file mode 100644 index 00000000000000..ea9f4f87b2bad9 --- /dev/null +++ b/editor/block-switcher/test/index.js @@ -0,0 +1,167 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import { spy } from 'sinon'; + +/** + * Internal dependencies + */ +import { BlockSwitcher } from '../'; +import { createBlock, getBlockType, getBlockTypes, unregisterBlockType, setUnknownTypeHandler, registerBlockType } from 'blocks'; + +describe( 'BlockSwitcher', () => { + describe( 'without any registered blocks', () => { + it( 'should return null without any blocks generated', () => { + const blockSwitcher = shallow( ); + // Render returns null when the allowedBlocks list is empty. + expect( blockSwitcher.equals( null ) ).to.be.true(); + } ); + } ); + + describe( 'state management and instance methods', () => { + it( 'should initialize with state.open being false', () => { + const blockSwitcher = shallow( ); + expect( blockSwitcher.state( 'open' ) ).to.be.false(); + } ); + + describe( '.toggleMenu()', () => { + it( 'should toggle state to open when closed', () => { + const blockSwitcher = shallow( ); + blockSwitcher.instance().toggleMenu(); + expect( blockSwitcher.state( 'open' ) ).to.be.true(); + } ); + + it( 'should toggle state to closed when open', () => { + const blockSwitcher = shallow( ); + blockSwitcher.setState( { open: true } ); + blockSwitcher.instance().toggleMenu(); + expect( blockSwitcher.state( 'open' ) ).to.be.false(); + } ); + } ); + + describe( '.handleClickOutside()', () => { + it( 'should return null if the menu is not open', () => { + const blockSwitcher = shallow( + + ); + blockSwitcher.setState( { open: false } ); + expect( blockSwitcher.instance().handleClickOutside() ).to.be.undefined(); + } ); + + it( 'should toggle state to closed when open', () => { + const blockSwitcher = shallow( + + ); + blockSwitcher.setState( { open: true } ); + blockSwitcher.instance().handleClickOutside(); + expect( blockSwitcher.state( 'open' ) ).to.be.false(); + } ); + } ); + + describe( '.switchBlockType()', () => { + it( 'should return a function that closes the menu and calls props.onTransform()', () => { + const name = 'doesnotexist'; + const block = { + name, + }; + const destinationName = 'alsodoesnotexist'; + const onTransform = spy(); + const blockSwitcher = shallow( + + ); + // Set state to open to verify later it closes to avoid false positive. + blockSwitcher.setState( { open: true } ); + + const switchBlockFunction = blockSwitcher.instance().switchBlockType( destinationName ); + expect( switchBlockFunction ).to.be.a( 'function' ); + + // Call the function with the destination blockType name already bound. + switchBlockFunction(); + expect( blockSwitcher.state( 'open' ) ).to.be.false(); + expect( onTransform ).to.have.been.calledOnce(); + expect( onTransform ).to.have.been.calledWith( block, destinationName ); + } ); + } ); + } ); + + describe( 'with registered blocks', () => { + before( () => { + registerBlockType( 'core/tester-block', { + icon: 'test', + transforms: { + to: [ { + blocks: [ 'core/text-block' ], + transform: ( { value } ) => { + return createBlock( 'core/text-block', { + value: 'text ' + value, + } ); + }, + } ], + }, + } ); + registerBlockType( 'core/text-block', { + icon: 'text', + title: 'Text', + } ); + } ); + + after( () => { + setUnknownTypeHandler( undefined ); + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.slug ); + } ); + } ); + + describe( 'rendering the menu', () => { + it( 'should only an IconButton when menu closed with matching props', () => { + const block = createBlock( 'core/tester-block' ); + const blockSwitcher = shallow( ); + const switcherMenu = blockSwitcher.find( '.editor-block-switcher__menu' ); + expect( switcherMenu.exists() ).to.be.false(); + + // The first IconButton is used to open and close menu. + const iconButton = blockSwitcher.find( 'IconButton' ); + expect( iconButton.props() ).to.include( { + className: 'editor-block-switcher__toggle', + icon: 'test', + onClick: blockSwitcher.instance().toggleMenu, + 'aria-haspopup': 'true', + 'aria-expanded': false, + label: 'Change block type', + } ); + } ); + + it( 'should render a menu of IconButtons for blocks to switch to, when open', () => { + const block = createBlock( 'core/tester-block' ); + const { icon, title } = getBlockType( 'core/text-block' ); + const blockSwitcher = shallow( ); + // Set the state to open. + blockSwitcher.setState( { open: true } ); + + // The first IconButton is used to open and close menu. + const mainButton = blockSwitcher.find( 'IconButton' ).first(); + // Aria expanded should trigger when the menu is open. + expect( mainButton.prop( 'aria-expanded' ) ).to.be.true(); + + // Verify menu exists with attributes. + const switcherMenu = blockSwitcher.find( '.editor-block-switcher__menu' ); + expect( switcherMenu.props() ).to.include( { + className: 'editor-block-switcher__menu', + role: 'menu', + tabIndex: '0', + 'aria-label': 'Block types', + } ); + + const iconButton = switcherMenu.find( 'IconButton' ); + expect( iconButton.props() ).to.include( { + className: 'editor-block-switcher__menu-item', + icon: icon, + role: 'menuitem', + } ); + expect( iconButton.children().text() ).to.equal( title ); + } ); + } ); + } ); +} );