UNPKG

@wordpress/block-editor

Version:
2,329 lines (2,153 loc) 105 kB
/** * WordPress dependencies */ import { registerBlockType, unregisterBlockType, setFreeformContentHandlerName, } from '@wordpress/blocks'; import { RawHTML } from '@wordpress/element'; import { symbol } from '@wordpress/icons'; import { select, dispatch } from '@wordpress/data'; /** * Internal dependencies */ import * as selectors from '../selectors'; import { store } from '../'; import { lock } from '../../lock-unlock'; const { getBlockName, getBlock, getBlocks, getBlockParents, getBlockParentsByBlockName, getBlockCount, getClientIdsWithDescendants, getClientIdsOfDescendants, hasSelectedBlock, getSelectedBlock, getSelectedBlockClientId, getBlockRootClientId, getBlockHierarchyRootClientId, getGlobalBlockCount, getSelectedBlockClientIds, getMultiSelectedBlockClientIds, getMultiSelectedBlocks, getMultiSelectedBlocksStartClientId, getMultiSelectedBlocksEndClientId, getBlockOrder, getBlockIndex, getPreviousBlockClientId, getNextBlockClientId, isBlockSelected, hasSelectedInnerBlock, isBlockWithinSelection, hasMultiSelection, isBlockMultiSelected, isFirstMultiSelectedBlock, getBlockMode, isTyping, isDraggingBlocks, getDraggedBlockClientIds, isBlockBeingDragged, isAncestorBeingDragged, getBlockInsertionPoint, isBlockInsertionPointVisible, isSelectionEnabled, canInsertBlockType, canInsertBlocks, getBlockTransformItems, isValidTemplate, getTemplate, getTemplateLock, getBlockListSettings, __experimentalGetBlockListSettingsForBlocks, __experimentalGetLastBlockAttributeChanges, getLowestCommonAncestorWithSelectedBlock, __experimentalGetActiveBlockIdByBlockNames: getActiveBlockIdByBlockNames, __unstableGetClientIdWithClientIdsTree, __unstableGetClientIdsTree, wasBlockJustInserted, getBlocksByName, getBlockEditingMode, } = selectors; describe( 'selectors', () => { let cachedSelectors; beforeAll( () => { cachedSelectors = Object.entries( selectors ) .filter( ( [ , selector ] ) => selector.clear ) .map( ( [ , selector ] ) => selector ); } ); beforeEach( () => { registerBlockType( 'core/block', { save: () => null, category: 'reusable', title: 'Reusable Block Stub', supports: { inserter: false, }, } ); registerBlockType( 'core/test-block-a', { save: ( props ) => props.attributes.text, category: 'design', title: 'Test Block A', icon: 'test', keywords: [ 'testing' ], } ); registerBlockType( 'core/test-block-b', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block B', icon: 'test', keywords: [ 'testing' ], supports: { multiple: false, }, } ); registerBlockType( 'core/test-block-c', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block C', icon: 'test', keywords: [ 'testing' ], parent: [ 'core/test-block-b' ], } ); registerBlockType( 'core/freeform', { save: ( props ) => <RawHTML>{ props.attributes.content }</RawHTML>, category: 'text', title: 'Test Freeform Content Handler', icon: 'test', attributes: { content: { type: 'string', }, }, } ); registerBlockType( 'core/post-content-child', { save: () => null, category: 'text', title: 'Test Block Post Content Child', icon: 'test', keywords: [ 'testing' ], parent: [ 'core/post-content' ], } ); registerBlockType( 'core/test-block-ancestor', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block required as ancestor', icon: 'test', keywords: [ 'testing' ], } ); registerBlockType( 'core/test-block-parent', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block required as parent', icon: 'test', keywords: [ 'testing' ], } ); registerBlockType( 'core/test-block-requires-ancestor', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block that requires ancestor', icon: 'test', keywords: [ 'testing' ], ancestor: [ 'core/test-block-ancestor' ], } ); registerBlockType( 'core/test-block-requires-ancestor-parent', { save: ( props ) => props.attributes.text, category: 'text', title: 'Test Block that requires both ancestor and parent', icon: 'test', keywords: [ 'testing' ], parent: [ 'core/test-block-parent' ], ancestor: [ 'core/test-block-ancestor' ], } ); setFreeformContentHandlerName( 'core/freeform' ); cachedSelectors.forEach( ( { clear } ) => clear() ); } ); afterEach( () => { unregisterBlockType( 'core/block' ); unregisterBlockType( 'core/test-block-a' ); unregisterBlockType( 'core/test-block-b' ); unregisterBlockType( 'core/test-block-c' ); unregisterBlockType( 'core/freeform' ); unregisterBlockType( 'core/post-content-child' ); unregisterBlockType( 'core/test-block-ancestor' ); unregisterBlockType( 'core/test-block-parent' ); unregisterBlockType( 'core/test-block-requires-ancestor' ); unregisterBlockType( 'core/test-block-requires-ancestor-parent' ); setFreeformContentHandlerName( undefined ); } ); describe( 'getBlockName', () => { it( 'returns null if no block by clientId', () => { const state = { blocks: { byClientId: new Map(), attributes: {}, order: new Map(), parents: new Map(), }, }; const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); expect( name ).toBe( null ); } ); it( 'returns block name', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', name: 'core/paragraph', }, } ) ), attributes: new Map( Object.entries( { 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, } ) ), order: new Map( Object.entries( { '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], } ) ), parents: new Map( Object.entries( { 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': '', } ) ), }, }; const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); expect( name ).toBe( 'core/paragraph' ); } ); } ); describe( 'getBlock', () => { it( 'should return the block', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 123: {}, } ) ), order: new Map( Object.entries( { '': [ '123' ], 123: [], } ) ), parents: new Map( Object.entries( { 123: '', } ) ), tree: new Map( Object.entries( { 123: { clientId: '123', name: 'core/paragraph', attributes: {}, innerBlocks: [], }, } ) ), controlledInnerBlocks: {}, }, }; expect( getBlock( state, '123' ) ).toBe( state.blocks.tree.get( '123' ) ); } ); it( 'should return null if the block is not present in state', () => { const state = { blocks: { byClientId: new Map(), attributes: new Map(), order: new Map(), parents: new Map(), tree: new Map( Object.entries( { 123: { clientId: '123', name: 'core/paragraph', attributes: {}, innerBlocks: [], }, } ) ), controlledInnerBlocks: {}, }, }; expect( getBlock( state, '123' ) ).toBe( null ); } ); } ); describe( 'getBlocks', () => { it( 'should return the ordered blocks', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading' }, 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 23: {}, 123: {}, } ) ), order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 123: '', 23: '', } ) ), tree: new Map( Object.entries( { '': { innerBlocks: [ { clientId: '123', name: 'core/paragraph', attributes: {}, innerBlocks: [], }, { clientId: '23', name: 'core/heading', attributes: {}, innerBlocks: [], }, ], }, 123: {}, 23: {}, } ) ), controlledInnerBlocks: {}, }, }; expect( getBlocks( state ) ).toBe( state.blocks.tree.get( '' ).innerBlocks ); } ); } ); describe( 'getBlockParents', () => { it( 'should return parent blocks', () => { const state = { blocks: { parents: new Map( Object.entries( { 'client-id-01': '', 'client-id-02': 'client-id-01', 'client-id-03': 'client-id-02', 'client-id-04': 'client-id-03', } ) ), byClientId: new Map( Object.entries( { 'client-id-01': { clientId: 'client-id-01', name: 'core/columns', }, 'client-id-02': { clientId: 'client-id-02', name: 'core/navigation', }, 'client-id-03': { clientId: 'client-id-03', name: 'core/navigation-link', }, 'client-id-04': { clientId: 'client-id-04', name: 'core/paragraph', }, } ) ), cache: { 'client-id-01': {}, 'client-id-02': {}, 'client-id-03': {}, 'client-id-04': {}, }, controlledInnerBlocks: {}, }, }; expect( getBlockParents( state, 'client-id-04' ) ).toEqual( [ 'client-id-01', 'client-id-02', 'client-id-03', ] ); expect( getBlockParents( state, 'client-id-0' ) ).toEqual( [] ); } ); } ); describe( 'getBlockParentsByBlockName', () => { const state = { blocks: { parents: new Map( Object.entries( { 'client-id-01': '', 'client-id-02': 'client-id-01', 'client-id-03': 'client-id-02', 'client-id-04': 'client-id-03', 'client-id-05': 'client-id-04', } ) ), byClientId: new Map( Object.entries( { 'client-id-01': { clientId: 'client-id-01', name: 'core/navigation', }, 'client-id-02': { clientId: 'client-id-02', name: 'core/columns', }, 'client-id-03': { clientId: 'client-id-03', name: 'core/navigation', }, 'client-id-04': { clientId: 'client-id-04', name: 'core/navigation-link', }, 'client-id-05': { clientId: 'client-id-05', name: 'core/navigation-link', }, } ) ), cache: { 'client-id-01': {}, 'client-id-02': {}, 'client-id-03': {}, 'client-id-04': {}, 'client-id-05': {}, }, controlledInnerBlocks: {}, }, }; it( 'should return parent blocks', () => { expect( getBlockParentsByBlockName( state, 'client-id-05', 'core/navigation' ) ).toEqual( [ 'client-id-01', 'client-id-03' ] ); expect( getBlockParentsByBlockName( state, 'client-id-05', 'core/columns' ) ).toEqual( [ 'client-id-02' ] ); expect( getBlockParentsByBlockName( state, 'client-id-5', 'core/unknown-block' ) ).toEqual( [] ); } ); it( 'Should optionally accept an array of parent types and return parents of multiple types', () => { expect( getBlockParentsByBlockName( state, 'client-id-05', [ 'core/navigation', ] ) ).toEqual( [ 'client-id-01', 'client-id-03' ] ); expect( getBlockParentsByBlockName( state, 'client-id-05', [ 'core/columns', 'core/navigation', ] ) ).toEqual( [ 'client-id-01', 'client-id-02', 'client-id-03' ] ); } ); } ); describe( 'getClientIdsOfDescendants', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 'uuid-2': { clientId: 'uuid-2', name: 'core/image', }, 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', }, 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', }, 'uuid-8': { clientId: 'uuid-8', name: 'core/block', }, 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', }, 'uuid-12': { clientId: 'uuid-12', name: 'core/column', }, 'uuid-14': { clientId: 'uuid-14', name: 'core/column', }, 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', }, 'uuid-18': { clientId: 'uuid-18', name: 'core/block', }, 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', }, 'uuid-22': { clientId: 'uuid-22', name: 'core/block', }, 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', }, 'uuid-26': { clientId: 'uuid-26', name: 'core/column', }, 'uuid-28': { clientId: 'uuid-28', name: 'core/column', }, 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', }, } ) ), attributes: new Map( Object.entries( { 'uuid-2': {}, 'uuid-4': {}, 'uuid-6': {}, 'uuid-8': {}, 'uuid-10': {}, 'uuid-12': {}, 'uuid-14': {}, 'uuid-16': {}, 'uuid-18': {}, 'uuid-20': {}, 'uuid-22': {}, 'uuid-24': {}, 'uuid-26': {}, 'uuid-28': {}, 'uuid-30': {}, } ) ), order: new Map( Object.entries( { '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], 'uuid-2': [], 'uuid-4': [], 'uuid-6': [], 'uuid-8': [], 'uuid-10': [ 'uuid-12', 'uuid-14' ], 'uuid-12': [ 'uuid-16' ], 'uuid-14': [ 'uuid-18' ], 'uuid-16': [], 'uuid-18': [ 'uuid-24' ], 'uuid-20': [], 'uuid-22': [], 'uuid-24': [ 'uuid-26', 'uuid-28' ], 'uuid-26': [], 'uuid-28': [ 'uuid-30' ], } ) ), parents: new Map( Object.entries( { 'uuid-6': '', 'uuid-8': '', 'uuid-10': '', 'uuid-22': '', 'uuid-12': 'uuid-10', 'uuid-14': 'uuid-10', 'uuid-16': 'uuid-12', 'uuid-18': 'uuid-14', 'uuid-24': 'uuid-18', 'uuid-26': 'uuid-24', 'uuid-28': 'uuid-24', 'uuid-30': 'uuid-28', } ) ), controlledInnerBlocks: {}, }, }; it( 'should return the ids of any descendants in sequential order, given an array of clientIds', () => { expect( getClientIdsOfDescendants( state, [ 'uuid-10' ] ) ).toEqual( [ 'uuid-12', 'uuid-16', 'uuid-14', 'uuid-18', 'uuid-24', 'uuid-26', 'uuid-28', 'uuid-30', ] ); } ); it( 'should return same value when called with same state and argument', () => { expect( getClientIdsOfDescendants( state, 'uuid-10' ) ).toBe( getClientIdsOfDescendants( state, 'uuid-10' ) ); } ); } ); describe( 'getClientIdsWithDescendants', () => { it( 'should return the ids for top-level blocks and their descendants of any depth (for nested blocks) in sequential order.', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 'uuid-2': { clientId: 'uuid-2', name: 'core/image', }, 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', }, 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', }, 'uuid-8': { clientId: 'uuid-8', name: 'core/block', }, 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', }, 'uuid-12': { clientId: 'uuid-12', name: 'core/column', }, 'uuid-14': { clientId: 'uuid-14', name: 'core/column', }, 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', }, 'uuid-18': { clientId: 'uuid-18', name: 'core/block', }, 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', }, 'uuid-22': { clientId: 'uuid-22', name: 'core/block', }, 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', }, 'uuid-26': { clientId: 'uuid-26', name: 'core/column', }, 'uuid-28': { clientId: 'uuid-28', name: 'core/column', }, 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', }, } ) ), attributes: new Map( Object.entries( { 'uuid-2': {}, 'uuid-4': {}, 'uuid-6': {}, 'uuid-8': {}, 'uuid-10': {}, 'uuid-12': {}, 'uuid-14': {}, 'uuid-16': {}, 'uuid-18': {}, 'uuid-20': {}, 'uuid-22': {}, 'uuid-24': {}, 'uuid-26': {}, 'uuid-28': {}, 'uuid-30': {}, } ) ), order: new Map( Object.entries( { '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], 'uuid-2': [], 'uuid-4': [], 'uuid-6': [], 'uuid-8': [], 'uuid-10': [ 'uuid-12', 'uuid-14' ], 'uuid-12': [ 'uuid-16' ], 'uuid-14': [ 'uuid-18' ], 'uuid-16': [], 'uuid-18': [ 'uuid-24' ], 'uuid-20': [], 'uuid-22': [], 'uuid-24': [ 'uuid-26', 'uuid-28' ], 'uuid-26': [], 'uuid-28': [ 'uuid-30' ], } ) ), parents: new Map( Object.entries( { 'uuid-6': '', 'uuid-8': '', 'uuid-10': '', 'uuid-22': '', 'uuid-12': 'uuid-10', 'uuid-14': 'uuid-10', 'uuid-16': 'uuid-12', 'uuid-18': 'uuid-14', 'uuid-24': 'uuid-18', 'uuid-26': 'uuid-24', 'uuid-28': 'uuid-24', 'uuid-30': 'uuid-28', } ) ), }, }; expect( getClientIdsWithDescendants( state ) ).toEqual( [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-12', 'uuid-16', 'uuid-14', 'uuid-18', 'uuid-24', 'uuid-26', 'uuid-28', 'uuid-30', 'uuid-22', ] ); } ); } ); describe( 'getBlockCount', () => { it( 'should return the number of top-level blocks in the post', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading' }, 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 23: {}, 123: {}, } ) ), order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), }, }; expect( getBlockCount( state ) ).toBe( 2 ); } ); it( 'should return the number of blocks in a nested context', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 123: { clientId: '123', name: 'core/columns' }, 456: { clientId: '456', name: 'core/paragraph' }, 789: { clientId: '789', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 123: {}, 456: {}, 789: {}, } ) ), order: new Map( Object.entries( { '': [ '123' ], 123: [ '456', '789' ], } ) ), parents: new Map( Object.entries( { 123: '', 456: '123', 789: '123', } ) ), }, }; expect( getBlockCount( state, '123' ) ).toBe( 2 ); } ); } ); describe( 'hasSelectedBlock', () => { it( 'should return false if no selection', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( hasSelectedBlock( state ) ).toBe( false ); } ); it( 'should return false if multi-selection', () => { const state = { selection: { selectionStart: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, selectionEnd: { clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', }, }, }; expect( hasSelectedBlock( state ) ).toBe( false ); } ); it( 'should return true if singular selection', () => { const state = { selection: { selectionStart: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, selectionEnd: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, }, }; expect( hasSelectedBlock( state ) ).toBe( true ); } ); } ); describe( 'getGlobalBlockCount', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 123: { clientId: '123', name: 'core/heading' }, 456: { clientId: '456', name: 'core/paragraph' }, 789: { clientId: '789', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 123: {}, 456: {}, 789: {}, } ) ), order: new Map( Object.entries( { '': [ '123', '456' ], } ) ), parents: new Map( Object.entries( { 123: '', 456: '', } ) ), }, }; it( 'should return the global number of blocks in the post', () => { expect( getGlobalBlockCount( state ) ).toBe( 2 ); } ); it( 'should return the global number of blocks in the post of a given type', () => { expect( getGlobalBlockCount( state, 'core/paragraph' ) ).toBe( 1 ); } ); it( 'should return 0 if no blocks exist', () => { const emptyState = { blocks: { byClientId: new Map(), attributes: new Map(), order: new Map(), parents: new Map(), }, }; expect( getGlobalBlockCount( emptyState ) ).toBe( 0 ); expect( getGlobalBlockCount( emptyState, 'core/heading' ) ).toBe( 0 ); } ); } ); describe( 'getBlocksByName', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 123: { clientId: '123', name: 'core/heading' }, 456: { clientId: '456', name: 'core/paragraph' }, 789: { clientId: '789', name: 'core/paragraph' }, 1011: { clientId: '1011', name: 'core/group' }, 1213: { clientId: '1213', name: 'core/paragraph' }, 1415: { clientId: '1213', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 123: {}, 456: {}, 789: {}, 1011: {}, 1213: {}, 1415: {}, } ) ), order: new Map( Object.entries( { '': [ '123', '456', '1011' ], 1011: [ '1415', '1213' ], } ) ), parents: new Map( Object.entries( { 123: '', 456: '', 1011: '', 1213: '1011', 1415: '1011', } ) ), }, }; it( 'should return the clientIds of blocks of a given type', () => { expect( getBlocksByName( state, 'core/heading' ) ).toStrictEqual( [ '123', ] ); } ); it( 'should return the clientIds of blocks of a given type even if blocks are nested', () => { expect( getBlocksByName( state, 'core/paragraph' ) ).toStrictEqual( [ '456', '1415', '1213' ] ); } ); it( 'Should return empty array if no blocks match. The empty array should be the same reference', () => { const result = getBlocksByName( state, 'test/missing' ); expect( getBlocksByName( state, 'test/missing' ) ).toStrictEqual( [] ); expect( getBlocksByName( state, 'test/missing2' ) === result ).toBe( true ); } ); } ); describe( 'getSelectedBlockClientId', () => { it( 'should return null if no block is selected', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getSelectedBlockClientId( state ) ).toBe( null ); } ); it( 'should return null if there is multi selection', () => { const state = { selection: { selectionStart: { clientId: '23' }, selectionEnd: { clientId: '123' }, }, }; expect( getSelectedBlockClientId( state ) ).toBe( null ); } ); it( 'should return the selected block ClientId', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { name: 'fake block', }, } ) ), }, selection: { selectionStart: { clientId: '23' }, selectionEnd: { clientId: '23' }, }, }; expect( getSelectedBlockClientId( state ) ).toEqual( '23' ); } ); } ); describe( 'getSelectedBlock', () => { it( 'should return null if no block is selected', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading' }, 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 23: {}, 123: {}, } ) ), order: new Map( Object.entries( { '': [ '23', '123' ], 23: [], 123: [], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), tree: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading', attributes: {}, innerBlocks: [], }, } ) ), }, selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getSelectedBlock( state ) ).toBe( null ); } ); it( 'should return null if there is multi selection', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading' }, 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 23: {}, 123: {}, } ) ), order: new Map( Object.entries( { '': [ '23', '123' ], 23: [], 123: [], } ) ), parents: new Map( Object.entries( { 123: '', 23: '', } ) ), tree: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading', attributes: {}, innerBlocks: [], }, } ) ), }, selection: { selectionStart: { clientId: '23' }, selectionEnd: { clientId: '123' }, }, }; expect( getSelectedBlock( state ) ).toBe( null ); } ); it( 'should return the selected block', () => { const state = { blocks: { byClientId: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading' }, 123: { clientId: '123', name: 'core/paragraph' }, } ) ), attributes: new Map( Object.entries( { 23: {}, 123: {}, } ) ), order: new Map( Object.entries( { '': [ '23', '123' ], 23: [], 123: [], } ) ), parents: new Map( Object.entries( { 123: '', 23: '', } ) ), tree: new Map( Object.entries( { 23: { clientId: '23', name: 'core/heading', attributes: {}, innerBlocks: [], }, } ) ), controlledInnerBlocks: {}, }, selection: { selectionStart: { clientId: '23' }, selectionEnd: { clientId: '23' }, }, }; expect( getSelectedBlock( state ) ).toEqual( getBlock( state, '23' ) ); } ); } ); describe( 'getBlockRootClientId', () => { it( 'should return null if the block does not exist', () => { const state = { blocks: { order: new Map(), parents: new Map(), }, }; expect( getBlockRootClientId( state, 56 ) ).toBeNull(); } ); it( 'should return root ClientId relative the block ClientId', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 123: '', 23: '', 456: '123', 56: '123', } ) ), }, }; expect( getBlockRootClientId( state, '56' ) ).toBe( '123' ); } ); } ); describe( 'getBlockHierarchyRootClientId', () => { it( 'should return the given block if the block has no parents', () => { const state = { blocks: { order: new Map(), parents: new Map(), }, }; expect( getBlockHierarchyRootClientId( state, '56' ) ).toBe( '56' ); } ); it( 'should return root ClientId relative the block ClientId', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ 'a', 'b' ], a: [ 'c', 'd' ], } ) ), parents: new Map( Object.entries( { a: '', b: '', c: 'a', d: 'a', } ) ), }, }; expect( getBlockHierarchyRootClientId( state, 'c' ) ).toBe( 'a' ); } ); it( 'should return the top level root ClientId relative the block ClientId', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ 'a', 'b' ], a: [ 'c', 'd' ], d: [ 'e' ], } ) ), parents: new Map( Object.entries( { a: '', b: '', c: 'a', d: 'a', e: 'd', } ) ), }, }; expect( getBlockHierarchyRootClientId( state, 'e' ) ).toBe( 'a' ); } ); } ); describe( 'getSelectedBlockClientIds', () => { it( 'should return empty if there is no selection', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 123: '', 23: '', } ) ), }, selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getSelectedBlockClientIds( state ) ).toEqual( [] ); } ); it( 'should return the selected block clientId if there is a selection', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '2' }, }, }; expect( getSelectedBlockClientIds( state ) ).toEqual( [ '2' ] ); } ); it( 'should return selected block clientIds if there is multi selection', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; expect( getSelectedBlockClientIds( state ) ).toEqual( [ '4', '3', '2', ] ); } ); it( 'should return selected block clientIds if there is multi selection (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], 4: [ '9', '8', '7', '6' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '4', 7: '4', 8: '4', 9: '4', } ) ), }, selection: { selectionStart: { clientId: '7' }, selectionEnd: { clientId: '9' }, }, }; expect( getSelectedBlockClientIds( state ) ).toEqual( [ '9', '8', '7', ] ); } ); } ); describe( 'getMultiSelectedBlockClientIds', () => { it( 'should return empty if there is no multi selection', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [] ); } ); it( 'should return selected block clientIds if there is multi selection', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ '4', '3', '2', ] ); } ); it( 'should return selected block clientIds if there is multi selection (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], 4: [ '9', '8', '7', '6' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '4', 7: '4', 8: '4', 9: '4', } ) ), }, selection: { selectionStart: { clientId: '7' }, selectionEnd: { clientId: '9' }, }, }; expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ '9', '8', '7', ] ); } ); } ); describe( 'getMultiSelectedBlocks', () => { it( 'should return the same reference on subsequent invocations of empty selection', () => { const state = { blocks: { byClientId: new Map(), attributes: new Map(), order: new Map(), parents: new Map(), }, selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getMultiSelectedBlocks( state ) ).toBe( getMultiSelectedBlocks( state ) ); } ); } ); describe( 'getMultiSelectedBlocksStartClientId', () => { it( 'returns null if there is no multi selection', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getMultiSelectedBlocksStartClientId( state ) ).toBeNull(); } ); it( 'returns multi selection start', () => { const state = { selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; expect( getMultiSelectedBlocksStartClientId( state ) ).toBe( '2' ); } ); } ); describe( 'getMultiSelectedBlocksEndClientId', () => { it( 'returns null if there is no multi selection', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( getMultiSelectedBlocksEndClientId( state ) ).toBeNull(); } ); it( 'returns multi selection end', () => { const state = { selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; expect( getMultiSelectedBlocksEndClientId( state ) ).toBe( '4' ); } ); } ); describe( 'getBlockOrder', () => { it( 'should return the ordered block ClientIds of top-level blocks by default', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getBlockOrder( state ) ).toEqual( [ '123', '23' ] ); } ); it( 'should return the ordered block ClientIds at a specified rootClientId', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 456: '123', } ) ), }, }; expect( getBlockOrder( state, '123' ) ).toEqual( [ '456' ] ); } ); } ); describe( 'getBlockIndex', () => { it( 'should return the block order', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getBlockIndex( state, '23' ) ).toBe( 1 ); } ); it( 'should return the block order (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 56: '123', 456: '123', } ) ), }, }; expect( getBlockIndex( state, '56' ) ).toBe( 1 ); } ); } ); describe( 'getPreviousBlockClientId', () => { it( 'should return the previous block', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getPreviousBlockClientId( state, '23' ) ).toEqual( '123' ); } ); it( 'should return the previous block (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 456: '123', 56: '123', } ) ), }, }; expect( getPreviousBlockClientId( state, '56', '123' ) ).toEqual( '456' ); } ); it( 'should return null for the first block', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getPreviousBlockClientId( state, '123' ) ).toBeNull(); } ); it( 'should return null for the first block (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 456: '123', 56: '123', } ) ), }, }; expect( getPreviousBlockClientId( state, '456', '123' ) ).toBeNull(); } ); } ); describe( 'getNextBlockClientId', () => { it( 'should return the following block', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getNextBlockClientId( state, '123' ) ).toEqual( '23' ); } ); it( 'should return the following block (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 456: '123', 56: '123', } ) ), }, }; expect( getNextBlockClientId( state, '456', '123' ) ).toEqual( '56' ); } ); it( 'should return null for the last block', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', } ) ), }, }; expect( getNextBlockClientId( state, '23' ) ).toBeNull(); } ); it( 'should return null for the last block (nested context)', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '123', '23' ], 123: [ '456', '56' ], } ) ), parents: new Map( Object.entries( { 23: '', 123: '', 456: '123', 56: '123', } ) ), }, }; expect( getNextBlockClientId( state, '56', '123' ) ).toBeNull(); } ); } ); describe( 'isBlockSelected', () => { it( 'should return true if the block is selected', () => { const state = { selection: { selectionStart: { clientId: '123' }, selectionEnd: { clientId: '123' }, }, }; expect( isBlockSelected( state, '123' ) ).toBe( true ); } ); it( 'should return false if a multi-selection range exists', () => { const state = { selection: { selectionStart: { clientId: '123' }, selectionEnd: { clientId: '124' }, }, }; expect( isBlockSelected( state, '123' ) ).toBe( false ); } ); it( 'should return false if the block is not selected', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( isBlockSelected( state, '23' ) ).toBe( false ); } ); } ); describe( 'hasSelectedInnerBlock', () => { it( 'should return false if the selected block is a child of the given ClientId', () => { const state = { selection: { selectionStart: { clientId: '5' }, selectionEnd: { clientId: '5' }, }, blocks: { order: new Map( Object.entries( { 4: [ '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '4', 2: '4', 3: '4', } ) ), }, }; expect( hasSelectedInnerBlock( state, '4' ) ).toBe( false ); } ); it( 'should return true if the selected block is a child of the given ClientId', () => { const state = { selection: { selectionStart: { clientId: '3' }, selectionEnd: { clientId: '3' }, }, blocks: { order: new Map( Object.entries( { 4: [ '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '4', 2: '4', 3: '4', } ) ), }, }; expect( hasSelectedInnerBlock( state, '4' ) ).toBe( true ); } ); it( 'should return true if a multi selection exists that contains children of the block with the given ClientId', () => { const state = { blocks: { order: new Map( Object.entries( { 6: [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '6', 2: '6', 3: '6', 4: '6', 5: '6', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; expect( hasSelectedInnerBlock( state, '6' ) ).toBe( true ); } ); it( 'should return false if a multi selection exists bot does not contains children of the block with the given ClientId', () => { const state = { blocks: { order: new Map( Object.entries( { 3: [ '2', '1' ], 6: [ '5', '4' ], } ) ), parents: new Map( Object.entries( { 1: '3', 2: '3', 4: '6', 5: '6', } ) ), }, selection: { selectionStart: { clientId: '5' }, selectionEnd: { clientId: '4' }, }, }; expect( hasSelectedInnerBlock( state, '3' ) ).toBe( false ); } ); } ); describe( 'isBlockWithinSelection', () => { it( 'should return true if the block is selected but not the last', () => { const state = { selection: { selectionStart: { clientId: '5' }, selectionEnd: { clientId: '3' }, }, blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, }; expect( isBlockWithinSelection( state, '4' ) ).toBe( true ); } ); it( 'should return false if the block is the last selected', () => { const state = { selection: { selectionStart: { clientId: '5' }, selectionEnd: { clientId: '3' }, }, blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, }; expect( isBlockWithinSelection( state, '3' ) ).toBe( false ); } ); it( 'should return false if the block is not selected', () => { const state = { selection: { selectionStart: { clientId: '5' }, selectionEnd: { clientId: '3' }, }, blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, }; expect( isBlockWithinSelection( state, '2' ) ).toBe( false ); } ); it( 'should return false if there is no selection', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, }; expect( isBlockWithinSelection( state, '4' ) ).toBe( false ); } ); } ); describe( 'hasMultiSelection', () => { it( 'should return false if no selection', () => { const state = { selection: { selectionStart: {}, selectionEnd: {}, }, }; expect( hasMultiSelection( state ) ).toBe( false ); } ); it( 'should return false if singular selection', () => { const state = { selection: { selectionStart: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, selectionEnd: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, }, }; expect( hasMultiSelection( state ) ).toBe( false ); } ); it( 'should return true if multi-selection', () => { const state = { selection: { selectionStart: { clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', }, selectionEnd: { clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', }, }, }; expect( hasMultiSelection( state ) ).toBe( true ); } ); } ); describe( 'isBlockMultiSelected', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; it( 'should return true if the block is multi selected', () => { expect( isBlockMultiSelected( state, '3' ) ).toBe( true ); } ); it( 'should return false if the block is not multi selected', () => { expect( isBlockMultiSelected( state, '5' ) ).toBe( false ); } ); } ); describe( 'isFirstMultiSelectedBlock', () => { const state = { blocks: { order: new Map( Object.entries( { '': [ '5', '4', '3', '2', '1' ], } ) ), parents: new Map( Object.entries( { 1: '', 2: '', 3: '', 4: '', 5: '', } ) ), }, selection: { selectionStart: { clientId: '2' }, selectionEnd: { clientId: '4' }, }, }; it( 'should return true if the block is first in multi selection', () => { expect( isFirstMultiSelectedBlock( state, '4' ) ).toBe( true ); } ); it( 'should return false if the block is not first in multi selection', () => { expect( isFirstMultiSelectedBlock( state, '3' ) ).toBe( false ); } ); } ); describe( 'getBlockMode', () => { it( 'should return "visual" if unset', () => { const state = { blocksMode: {}, }; expect( getBlockMode( state, '123' ) ).toEqual( 'visual' ); } ); it( 'should return the block mode', () => { const state = { blocksMode: { 123: 'html', }, }; expect( getBlockMode( state, '123' ) ).toEqual( 'html' ); } ); } ); describe( 'isTyping', () => { it( 'should return the isTyping flag if the block is selected', () => { const state = { isTyping: true, }; expect( isTyping( state ) ).toBe( true ); } ); it( 'should return false if the block is not selected', () => { const state = { isTyping: false, }; expect( isTyping( state ) ).toBe( false ); } ); } ); describe( 'isDraggingBlocks', () => { it( 'should return true if a block is being dragged', () => { const state = { draggedBlocks: [ 'block-client-id' ], }; expect( isDraggingBlocks( state ) ).toBe( true ); } ); it( 'should return false if a block is not being dragged', () => { const state = { draggedBlocks: [], }; expect( isDraggingBlocks( state ) ).toBe( false ); } ); } ); describe( 'getDraggedBlockClientIds', () => { it( 'returns the draggedBlocks state', () => { const draggedBlocks = [ 'block-client-id' ]; const state = { draggedBlocks, }; expect( getDraggedBlockClientIds( state ) ).toBe( draggedBlocks ); } ); } ); describe( 'isBlockBeingDragged', () => { it( 'returns true if the given client id is one of the blocks being dragged', () => { const state = { draggedBlocks: [ 'block-1', 'block-2', 'block-3' ], }; expect( isBlockBeingDragged( state, 'block-2' ) ).toBe( true ); } ); it( 'returns false if the given client id is not one of the blocks being dragged', () => { const state = { draggedBlocks: [ 'block-1', 'block-2', 'block-3' ], }; expect( isBlockBeingDragged( state, 'block-4' ) ).toBe( false ); } ); it( 'returns false if no blocks are being dragged', () => { const state = { draggedBlocks: [], }; expect( isBlockBeingDragged( state, 'block-1' ) ).toBe( false ); } ); } ); describe( 'isAncestorBeingDragged', () => { it( 'returns true if the given client id is a child of one of the blocks being dragged', () =