UNPKG

@gechiui/block-editor

Version:
2,156 lines (1,981 loc) 73.4 kB
/** * External dependencies */ import { values, noop, omit } from 'lodash'; import deepFreeze from 'deep-freeze'; /** * GeChiUI dependencies */ import { registerBlockType, unregisterBlockType, createBlock, } from '@gechiui/blocks'; /** * Internal dependencies */ import { hasSameKeys, isUpdatingSameBlockAttribute, blocks, isTyping, draggedBlocks, isCaretWithinFormattedText, selection, initialPosition, isMultiSelecting, preferences, blocksMode, insertionPoint, template, blockListSettings, lastBlockAttributesChange, lastBlockInserted, } from '../reducer'; describe( 'state', () => { describe( 'hasSameKeys()', () => { it( 'returns false if two objects do not have the same keys', () => { const a = { foo: 10 }; const b = { bar: 10 }; expect( hasSameKeys( a, b ) ).toBe( false ); } ); it( 'returns false if two objects have the same keys', () => { const a = { foo: 10 }; const b = { foo: 20 }; expect( hasSameKeys( a, b ) ).toBe( true ); } ); } ); describe( 'isUpdatingSameBlockAttribute()', () => { it( 'should return false if not updating block attributes', () => { const action = { type: 'SELECT_BLOCK', clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', }; const previousAction = { type: 'SELECT_BLOCK', clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', }; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); } ); it( 'should return false if last action was not updating block attributes', () => { const action = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 10, }, }; const previousAction = { type: 'SELECT_BLOCK', clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', }; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); } ); it( 'should return false if not updating the same block', () => { const action = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 10, }, }; const previousAction = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], attributes: { foo: 20, }, }; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); } ); it( 'should return false if not updating the same block attributes', () => { const action = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 10, }, }; const previousAction = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { bar: 20, }, }; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); } ); it( 'should return false if no previous action', () => { const action = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 10, }, }; const previousAction = undefined; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); } ); it( 'should return true if updating the same block attributes', () => { const action = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 10, }, }; const previousAction = { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ '9db792c6-a25a-495d-adbd-97d56a4c4189' ], attributes: { foo: 20, }, }; expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( true ); } ); } ); describe( 'blocks()', () => { beforeAll( () => { registerBlockType( 'core/test-block', { save: noop, edit: noop, category: 'text', title: 'test block', } ); } ); afterAll( () => { unregisterBlockType( 'core/test-block' ); } ); describe( 'replace inner blocks', () => { beforeAll( () => { registerBlockType( 'core/test-parent-block', { save: noop, edit: noop, category: 'text', title: 'test parent block', } ); registerBlockType( 'core/test-child-block', { save: noop, edit: noop, category: 'text', title: 'test child block 1', attributes: { attr: { type: 'boolean', }, attr2: { type: 'string', }, }, } ); } ); afterAll( () => { unregisterBlockType( 'core/test-parent-block' ); unregisterBlockType( 'core/test-child-block' ); } ); it( 'can replace a child block', () => { const existingState = deepFreeze( { byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, }, attributes: { chicken: {}, 'chicken-child': { attr: true, }, }, order: { '': [ 'chicken' ], chicken: [ 'chicken-child' ], 'chicken-child': [], }, parents: { chicken: '', 'chicken-child': 'chicken', }, tree: { '': {}, chicken: {}, 'chicken-child': {}, }, controlledInnerBlocks: {}, } ); const newChildBlock = createBlock( 'core/test-child-block', { attr: false, attr2: 'perfect', } ); const { clientId: newChildBlockId } = newChildBlock; const action = { type: 'REPLACE_INNER_BLOCKS', rootClientId: 'chicken', blocks: [ newChildBlock ], }; const state = blocks( existingState, action ); expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, [ newChildBlockId ]: { clientId: newChildBlockId, name: 'core/test-child-block', isValid: true, }, }, attributes: { chicken: {}, [ newChildBlockId ]: { attr: false, attr2: 'perfect', }, }, order: { '': [ 'chicken' ], chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, parents: { [ newChildBlockId ]: 'chicken', chicken: '', }, controlledInnerBlocks: {}, } ); expect( state.tree.chicken ).not.toBe( existingState.tree.chicken ); } ); it( 'can insert a child block', () => { const existingState = deepFreeze( { byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, }, attributes: { chicken: {}, }, order: { '': [ 'chicken' ], chicken: [], }, parents: { chicken: '', }, tree: { '': { innerBlocks: [], }, chicken: {}, }, controlledInnerBlocks: {}, } ); const newChildBlock = createBlock( 'core/test-child-block', { attr: false, attr2: 'perfect', } ); const { clientId: newChildBlockId } = newChildBlock; const action = { type: 'REPLACE_INNER_BLOCKS', rootClientId: 'chicken', blocks: [ newChildBlock ], }; const state = blocks( existingState, action ); expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, [ newChildBlockId ]: { clientId: newChildBlockId, name: 'core/test-child-block', isValid: true, }, }, attributes: { chicken: {}, [ newChildBlockId ]: { attr: false, attr2: 'perfect', }, }, order: { '': [ 'chicken' ], chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, parents: { [ newChildBlockId ]: 'chicken', chicken: '', }, controlledInnerBlocks: {}, } ); expect( state.tree.chicken ).not.toBe( existingState.tree.chicken ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.chicken ); expect( state.tree.chicken.innerBlocks[ 0 ] ).toBe( state.tree[ newChildBlockId ] ); expect( state.tree[ newChildBlockId ] ).toEqual( { clientId: newChildBlockId, innerBlocks: [], isValid: true, name: 'core/test-child-block', attributes: { attr: false, attr2: 'perfect', }, } ); } ); it( 'can replace multiple child blocks', () => { const existingState = deepFreeze( { byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, 'chicken-child-2': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, }, attributes: { chicken: {}, 'chicken-child': { attr: true, }, 'chicken-child-2': { attr2: 'ok', }, }, order: { '': [ 'chicken' ], chicken: [ 'chicken-child', 'chicken-child-2' ], 'chicken-child': [], 'chicken-child-2': [], }, parents: { chicken: '', 'chicken-child': 'chicken', 'chicken-child-2': 'chicken', }, tree: {}, controlledInnerBlocks: {}, } ); const newChildBlock1 = createBlock( 'core/test-child-block', { attr: false, attr2: 'perfect', } ); const newChildBlock2 = createBlock( 'core/test-child-block', { attr: true, attr2: 'not-perfect', } ); const newChildBlock3 = createBlock( 'core/test-child-block', { attr2: 'hello', } ); const { clientId: newChildBlockId1 } = newChildBlock1; const { clientId: newChildBlockId2 } = newChildBlock2; const { clientId: newChildBlockId3 } = newChildBlock3; const action = { type: 'REPLACE_INNER_BLOCKS', rootClientId: 'chicken', blocks: [ newChildBlock1, newChildBlock2, newChildBlock3 ], }; const state = blocks( existingState, action ); expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, [ newChildBlockId1 ]: { clientId: newChildBlockId1, name: 'core/test-child-block', isValid: true, }, [ newChildBlockId2 ]: { clientId: newChildBlockId2, name: 'core/test-child-block', isValid: true, }, [ newChildBlockId3 ]: { clientId: newChildBlockId3, name: 'core/test-child-block', isValid: true, }, }, attributes: { chicken: {}, [ newChildBlockId1 ]: { attr: false, attr2: 'perfect', }, [ newChildBlockId2 ]: { attr: true, attr2: 'not-perfect', }, [ newChildBlockId3 ]: { attr2: 'hello', }, }, order: { '': [ 'chicken' ], chicken: [ newChildBlockId1, newChildBlockId2, newChildBlockId3, ], [ newChildBlockId1 ]: [], [ newChildBlockId2 ]: [], [ newChildBlockId3 ]: [], }, parents: { chicken: '', [ newChildBlockId1 ]: 'chicken', [ newChildBlockId2 ]: 'chicken', [ newChildBlockId3 ]: 'chicken', }, controlledInnerBlocks: {}, } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.chicken ); expect( state.tree.chicken.innerBlocks[ 0 ] ).toBe( state.tree[ newChildBlockId1 ] ); expect( state.tree.chicken.innerBlocks[ 1 ] ).toBe( state.tree[ newChildBlockId2 ] ); expect( state.tree.chicken.innerBlocks[ 2 ] ).toBe( state.tree[ newChildBlockId3 ] ); expect( state.tree[ newChildBlockId1 ] ).toEqual( { innerBlocks: [], clientId: newChildBlockId1, name: 'core/test-child-block', isValid: true, attributes: { attr: false, attr2: 'perfect', }, } ); } ); it( 'can replace a child block that has other children', () => { const existingState = deepFreeze( { byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, 'chicken-grand-child': { clientId: 'chicken-child', name: 'core/test-block', isValid: true, }, }, attributes: { chicken: {}, 'chicken-child': {}, 'chicken-grand-child': {}, }, order: { '': [ 'chicken' ], chicken: [ 'chicken-child' ], 'chicken-child': [ 'chicken-grand-child' ], 'chicken-grand-child': [], }, parents: { chicken: '', 'chicken-child': 'chicken', 'chicken-grand-child': 'chicken-child', }, tree: { chicken: {}, }, controlledInnerBlocks: {}, } ); const newChildBlock = createBlock( 'core/test-block' ); const { clientId: newChildBlockId } = newChildBlock; const action = { type: 'REPLACE_INNER_BLOCKS', rootClientId: 'chicken', blocks: [ newChildBlock ], }; const state = blocks( existingState, action ); expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, [ newChildBlockId ]: { clientId: newChildBlockId, name: 'core/test-block', isValid: true, }, }, attributes: { chicken: {}, [ newChildBlockId ]: {}, }, order: { '': [ 'chicken' ], chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, parents: { chicken: '', [ newChildBlockId ]: 'chicken', }, controlledInnerBlocks: {}, } ); // the block object of the parent should be updated expect( state.tree.chicken ).not.toBe( existingState.tree.chicken ); } ); } ); it( 'should return empty byClientId, attributes, order by default', () => { const state = blocks( undefined, {} ); expect( state ).toEqual( { byClientId: {}, attributes: {}, order: {}, parents: {}, isPersistentChange: true, isIgnoredChange: false, tree: {}, controlledInnerBlocks: {}, } ); } ); it( 'should key by reset blocks clientId', () => { [ undefined, blocks( undefined, {} ) ].forEach( ( original ) => { const state = blocks( original, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'bananas', innerBlocks: [] } ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 1 ); expect( values( state.byClientId )[ 0 ].clientId ).toBe( 'bananas' ); expect( state.order ).toEqual( { '': [ 'bananas' ], bananas: [], } ); expect( state.tree.bananas ).toEqual( { clientId: 'bananas', innerBlocks: [], } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.bananas ); } ); } ); it( 'should key by reset blocks clientId, including inner blocks', () => { const original = blocks( undefined, {} ); const state = blocks( original, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'bananas', innerBlocks: [ { clientId: 'apples', innerBlocks: [] }, ], }, ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 2 ); expect( state.order ).toEqual( { '': [ 'bananas' ], apples: [], bananas: [ 'apples' ], } ); } ); it( 'should insert block', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'INSERT_BLOCKS', blocks: [ { clientId: 'ribs', name: 'core/freeform', innerBlocks: [], }, ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 2 ); expect( values( state.byClientId )[ 1 ].clientId ).toBe( 'ribs' ); expect( state.order ).toEqual( { '': [ 'chicken', 'ribs' ], chicken: [], ribs: [], } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.chicken ); expect( state.tree[ '' ].innerBlocks[ 1 ] ).toBe( state.tree.ribs ); expect( state.tree.chicken ).toEqual( { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], } ); } ); it( 'should replace the block', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [ { clientId: 'wings', name: 'core/freeform', innerBlocks: [], }, ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 1 ); expect( values( state.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); expect( values( state.byClientId )[ 0 ].clientId ).toBe( 'wings' ); expect( state.order ).toEqual( { '': [ 'wings' ], wings: [], } ); expect( state.parents ).toEqual( { wings: '', } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.wings ); expect( state.tree.wings ).toEqual( { clientId: 'wings', name: 'core/freeform', innerBlocks: [], } ); } ); it( 'Replacing the block with an empty list should remove it', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 0 ); expect( state.tree[ '' ].innerBlocks ).toHaveLength( 0 ); } ); it( 'should replace the block and remove references to its inner blocks', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [ { clientId: 'child', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], }, ], } ); const state = blocks( original, { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [ { clientId: 'wings', name: 'core/freeform', innerBlocks: [], }, ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 1 ); expect( state.order ).toEqual( { '': [ 'wings' ], wings: [], } ); expect( state.parents ).toEqual( { wings: '', } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.wings ); expect( state.tree.wings ).toEqual( { clientId: 'wings', name: 'core/freeform', innerBlocks: [], } ); } ); it( 'should replace the nested block', () => { const nestedBlock = createBlock( 'core/test-block' ); const wrapperBlock = createBlock( 'core/test-block', {}, [ nestedBlock, ] ); const replacementBlock = createBlock( 'core/test-block' ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const state = blocks( original, { type: 'REPLACE_BLOCKS', clientIds: [ nestedBlock.clientId ], blocks: [ replacementBlock ], } ); expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], [ replacementBlock.clientId ]: [], } ); expect( state.parents ).toEqual( { [ wrapperBlock.clientId ]: '', [ replacementBlock.clientId ]: wrapperBlock.clientId, } ); expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 0 ] ).toBe( state.tree[ replacementBlock.clientId ] ); expect( state.tree[ replacementBlock.clientId ] ).toEqual( { clientId: replacementBlock.clientId, name: 'core/test-block', innerBlocks: [], attributes: {}, isValid: true, } ); } ); it( 'should replace the block even if the new block clientId is the same', () => { const originalState = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const replacedState = blocks( originalState, { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [ { clientId: 'chicken', name: 'core/freeform', innerBlocks: [], }, ], } ); expect( Object.keys( replacedState.byClientId ) ).toHaveLength( 1 ); expect( values( originalState.byClientId )[ 0 ].name ).toBe( 'core/test-block' ); expect( values( replacedState.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); expect( values( replacedState.byClientId )[ 0 ].clientId ).toBe( 'chicken' ); expect( replacedState.order ).toEqual( { '': [ 'chicken' ], chicken: [], } ); expect( originalState.tree.chicken ).not.toBe( replacedState.tree.chicken ); const nestedBlock = { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }; const wrapperBlock = createBlock( 'core/test-block', {}, [ nestedBlock, ] ); const replacementNestedBlock = { clientId: 'chicken', name: 'core/freeform', attributes: {}, innerBlocks: [], }; const originalNestedState = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const replacedNestedState = blocks( originalNestedState, { type: 'REPLACE_BLOCKS', clientIds: [ nestedBlock.clientId ], blocks: [ replacementNestedBlock ], } ); expect( replacedNestedState.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ replacementNestedBlock.clientId ], [ replacementNestedBlock.clientId ]: [], } ); expect( originalNestedState.byClientId.chicken.name ).toBe( 'core/test-block' ); expect( replacedNestedState.byClientId.chicken.name ).toBe( 'core/freeform' ); } ); it( 'should update the block', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, isValid: false, innerBlocks: [], }, ], } ); const state = blocks( deepFreeze( original ), { type: 'UPDATE_BLOCK', clientId: 'chicken', updates: { attributes: { content: 'ribs' }, isValid: true, }, } ); expect( state.byClientId.chicken ).toEqual( { clientId: 'chicken', name: 'core/test-block', isValid: true, } ); expect( state.attributes.chicken ).toEqual( { content: 'ribs', } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.chicken ); expect( state.tree.chicken ).toEqual( { clientId: 'chicken', name: 'core/test-block', innerBlocks: [], attributes: { content: 'ribs', }, isValid: true, } ); } ); it( 'should update the reusable block reference if the temporary id is swapped', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/block', attributes: { ref: 'random-clientId', }, isValid: false, innerBlocks: [], }, ], } ); const state = blocks( deepFreeze( original ), { type: 'SAVE_REUSABLE_BLOCK_SUCCESS', id: 'random-clientId', updatedId: 3, } ); expect( state.byClientId.chicken ).toEqual( { clientId: 'chicken', name: 'core/block', isValid: false, } ); expect( state.attributes.chicken ).toEqual( { ref: 3, } ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.chicken ); expect( state.tree.chicken ).toEqual( { clientId: 'chicken', name: 'core/block', isValid: false, innerBlocks: [], attributes: { ref: 3, }, } ); } ); it( 'should move the block up', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_UP', clientIds: [ 'ribs' ], } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.ribs ); expect( state.tree[ '' ].innerBlocks[ 1 ] ).toBe( state.tree.chicken ); expect( state.tree.chicken ).toBe( original.tree.chicken ); } ); it( 'should move the nested block up', () => { const movedBlock = createBlock( 'core/test-block' ); const siblingBlock = createBlock( 'core/test-block' ); const wrapperBlock = createBlock( 'core/test-block', {}, [ siblingBlock, movedBlock, ] ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_UP', clientIds: [ movedBlock.clientId ], rootClientId: wrapperBlock.clientId, } ); expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ movedBlock.clientId, siblingBlock.clientId, ], [ movedBlock.clientId ]: [], [ siblingBlock.clientId ]: [], } ); expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 0 ] ).toBe( state.tree[ movedBlock.clientId ] ); expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 1 ] ).toBe( state.tree[ siblingBlock.clientId ] ); expect( state.tree[ movedBlock.clientId ] ).toBe( original.tree[ movedBlock.clientId ] ); } ); it( 'should move multiple blocks up', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_UP', clientIds: [ 'ribs', 'veggies' ], } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken', ] ); } ); it( 'should move multiple nested blocks up', () => { const movedBlockA = createBlock( 'core/test-block' ); const movedBlockB = createBlock( 'core/test-block' ); const siblingBlock = createBlock( 'core/test-block' ); const wrapperBlock = createBlock( 'core/test-block', {}, [ siblingBlock, movedBlockA, movedBlockB, ] ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_UP', clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], rootClientId: wrapperBlock.clientId, } ); expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ movedBlockA.clientId, movedBlockB.clientId, siblingBlock.clientId, ], [ movedBlockA.clientId ]: [], [ movedBlockB.clientId ]: [], [ siblingBlock.clientId ]: [], } ); } ); it( 'should not move the first block up', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_UP', clientIds: [ 'chicken' ], } ); expect( state.order ).toBe( original.order ); } ); it( 'should move the block down', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_DOWN', clientIds: [ 'chicken' ], } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move the nested block down', () => { const movedBlock = createBlock( 'core/test-block' ); const siblingBlock = createBlock( 'core/test-block' ); const wrapperBlock = createBlock( 'core/test-block', {}, [ movedBlock, siblingBlock, ] ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_DOWN', clientIds: [ movedBlock.clientId ], rootClientId: wrapperBlock.clientId, } ); expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlock.clientId, ], [ movedBlock.clientId ]: [], [ siblingBlock.clientId ]: [], } ); } ); it( 'should move multiple blocks down', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_DOWN', clientIds: [ 'chicken', 'ribs' ], } ); expect( state.order[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs', ] ); } ); it( 'should move multiple nested blocks down', () => { const movedBlockA = createBlock( 'core/test-block' ); const movedBlockB = createBlock( 'core/test-block' ); const siblingBlock = createBlock( 'core/test-block' ); const wrapperBlock = createBlock( 'core/test-block', {}, [ movedBlockA, movedBlockB, siblingBlock, ] ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ wrapperBlock ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_DOWN', clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], rootClientId: wrapperBlock.clientId, } ); expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlockA.clientId, movedBlockB.clientId, ], [ movedBlockA.clientId ]: [], [ movedBlockB.clientId ]: [], [ siblingBlock.clientId ]: [], } ); } ); it( 'should not move the last block down', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_DOWN', clientIds: [ 'ribs' ], } ); expect( state.order ).toBe( original.order ); } ); it( 'should remove the block', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'REMOVE_BLOCKS', clientIds: [ 'chicken' ], } ); expect( state.order[ '' ] ).toEqual( [ 'ribs' ] ); expect( state.order ).not.toHaveProperty( 'chicken' ); expect( state.parents ).toEqual( { ribs: '', } ); expect( state.byClientId ).toEqual( { ribs: { clientId: 'ribs', name: 'core/test-block', }, } ); expect( state.attributes ).toEqual( { ribs: {}, } ); expect( state.tree[ '' ].innerBlocks ).toHaveLength( 1 ); } ); it( 'should remove multiple blocks', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'REMOVE_BLOCKS', clientIds: [ 'chicken', 'veggies' ], } ); expect( state.order[ '' ] ).toEqual( [ 'ribs' ] ); expect( state.order ).not.toHaveProperty( 'chicken' ); expect( state.order ).not.toHaveProperty( 'veggies' ); expect( state.parents ).toEqual( { ribs: '', } ); expect( state.byClientId ).toEqual( { ribs: { clientId: 'ribs', name: 'core/test-block', }, } ); expect( state.attributes ).toEqual( { ribs: {}, } ); } ); it( 'should cascade remove to include inner blocks', () => { const block = createBlock( 'core/test-block', {}, [ createBlock( 'core/test-block', {}, [ createBlock( 'core/test-block' ), ] ), ] ); const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ block ], } ); const state = blocks( original, { type: 'REMOVE_BLOCKS', clientIds: [ block.clientId ], } ); expect( state.byClientId ).toEqual( {} ); expect( state.order ).toEqual( { '': [], } ); expect( state.parents ).toEqual( {} ); } ); it( 'should insert at the specified index', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'loquat', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'INSERT_BLOCKS', index: 1, blocks: [ { clientId: 'persimmon', name: 'core/freeform', innerBlocks: [], }, ], } ); expect( Object.keys( state.byClientId ) ).toHaveLength( 3 ); expect( state.order[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat', ] ); } ); it( 'should move block to lower index', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'ribs' ], index: 0, } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies', ] ); } ); it( 'should move block to higher index', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'ribs' ], index: 2, } ); expect( state.order[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs', ] ); } ); it( 'should not move block if passed same index', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'ribs' ], index: 1, } ); expect( state.order[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies', ] ); } ); it( 'should move multiple blocks', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'ribs', 'veggies' ], index: 0, } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken', ] ); } ); it( 'should move multiple blocks to different parent', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'ribs', name: 'core/test-block', attributes: {}, innerBlocks: [], }, { clientId: 'veggies', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], } ); const state = blocks( original, { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'ribs', 'veggies' ], fromRootClientId: '', toRootClientId: 'chicken', index: 0, } ); expect( state.order[ '' ] ).toEqual( [ 'chicken' ] ); expect( state.order.chicken ).toEqual( [ 'ribs', 'veggies' ] ); } ); describe( 'blocks', () => { describe( 'byClientId', () => { it( 'should ignore updates to non-existent block', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.byClientId ).toBe( original.byClientId ); } ); it( 'should return with same reference if no changes in updates', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: { updated: true, }, innerBlocks: [], }, ], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.byClientId ).toBe( state.byClientId ); } ); } ); describe( 'attributes', () => { it( 'should return with attribute block updates', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.attributes.kumquat.updated ).toBe( true ); } ); it( 'should return with attribute block updates when attributes are unique by block', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { kumquat: { updated: true }, }, uniqueByBlock: true, } ); expect( state.attributes.kumquat.updated ).toBe( true ); } ); it( 'should accumulate attribute block updates', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: { updated: true, }, innerBlocks: [], }, ], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { moreUpdated: true, }, } ); expect( state.attributes.kumquat ).toEqual( { updated: true, moreUpdated: true, } ); } ); it( 'should ignore updates to non-existent block', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.attributes ).toBe( original.attributes ); } ); it( 'should return with same reference if no changes in updates', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: { updated: true, }, innerBlocks: [], }, ], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.attributes ).toBe( state.attributes ); } ); } ); describe( 'isPersistentChange', () => { it( 'should default a changing state to true', () => { const state = deepFreeze( blocks( undefined, {} ) ); expect( state.isPersistentChange ).toBe( true ); } ); it( 'should consider any non-exempt block change as persistent', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [], } ) ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.isPersistentChange ).toBe( true ); } ); it( 'should consider any non-exempt block change as persistent across unchanging actions', () => { let original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ) ); original = blocks( original, { type: 'NOOP', } ); original = blocks( original, { // While RECEIVE_BLOCKS changes state, it's considered // as ignored, confirmed by this test. type: 'RECEIVE_BLOCKS', blocks: [], } ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: false, }, } ); expect( state.isPersistentChange ).toBe( true ); } ); it( 'should consider same block attribute update as exempt', () => { let original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ) ); original = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: false, }, } ); const state = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); expect( state.isPersistentChange ).toBe( false ); } ); it( 'should flag an explicitly marked persistent change', () => { let original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ) ); original = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: false, }, } ); original = blocks( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: [ 'kumquat' ], attributes: { updated: true, }, } ); const state = blocks( original, { type: 'MARK_LAST_CHANGE_AS_PERSISTENT', } ); expect( state.isPersistentChange ).toBe( true ); } ); it( 'should retain reference for same state, same persistence', () => { const original = deepFreeze( blocks( undefined, { type: 'RESET_BLOCKS', blocks: [], } ) ); const state = blocks( original, { type: '__INERT__', } ); expect( state ).toBe( original ); } ); } ); describe( 'isIgnoredChange', () => { it( 'should consider received blocks as ignored change', () => { const resetState = blocks( undefined, { type: 'random action', } ); const state = blocks( resetState, { type: 'RECEIVE_BLOCKS', blocks: [ { clientId: 'kumquat', attributes: {}, innerBlocks: [], }, ], } ); expect( state.isIgnoredChange ).toBe( true ); } ); } ); describe( 'controlledInnerBlocks', () => { it( 'should remove the content of the block if it switches from controlled to uncontrolled or opposite', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: {}, innerBlocks: [ { clientId: 'child', name: 'core/test-block', attributes: {}, innerBlocks: [], }, ], }, ], } ); const state = blocks( original, { type: 'SET_HAS_CONTROLLED_INNER_BLOCKS', clientId: 'chicken', hasControlledInnerBlocks: true, } ); expect( state.controlledInnerBlocks.chicken ).toBe( true ); // The previous content of the block should be removed expect( state.byClientId.child ).toBeUndefined(); expect( state.tree.child ).toBeUndefined(); expect( state.tree.chicken.innerBlocks ).toEqual( [] ); } ); } ); } ); } ); describe( 'insertionPoint', () => { it( 'should default to null', () => { const state = insertionPoint( undefined, {} ); expect( state ).toBe( null ); } ); it( 'should set insertion point', () => { const state = insertionPoint( null, { type: 'SHOW_INSERTION_POINT', rootClientId: 'clientId1', index: 0, } ); expect( state ).toEqual( { rootClientId: 'clientId1', index: 0, } ); } ); it( 'should clear the insertion point', () => { const original = deepFreeze( { rootClientId: 'clientId1', index: 0, } ); const state = insertionPoint( original, { type: 'HIDE_INSERTION_POINT', } ); expect( state ).toBe( null ); } ); } ); describe( 'isTyping()', () => { it( 'should set the typing flag to true', () => { const state = isTyping( false, { type: 'START_TYPING', } ); expect( state ).toBe( true ); } ); it( 'should set the typing flag to false', () => { const state = isTyping( false, { type: 'STOP_TYPING', } ); expect( state ).toBe( false ); } ); } ); describe( 'draggedBlocks', () => { it( 'should store the dragged client ids when a user starts dragging blocks', () => { const clientIds = [ 'block-1', 'block-2', 'block-3' ]; const state = draggedBlocks( [], { type: 'START_DRAGGING_BLOCKS', clientIds, } ); expect( state ).toBe( clientIds ); } ); it( 'should set the state to an empty array when a user stops dragging blocks', () => { const previousState = [ 'block-1', 'block-2', 'block-3' ]; const state = draggedBlocks( previousState, { type: 'STOP_