diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index bcacdd68de76e..6bf2454fd03ac 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -776,6 +776,17 @@ Whether redo history exists. ## Actions +### initBlocks + +Returns an action object used in signalling that blocks state should be +intialized using a specified array of blocks, + +This action reset the undo/redo history + +*Parameters* + + * blocks: Array of blocks. + ### resetBlocks Returns an action object used in signalling that blocks state should be @@ -1019,4 +1030,14 @@ Returns an action object used in signalling that undo history should pop. ### createUndoLevel Returns an action object used in signalling that undo history record should -be created. \ No newline at end of file +be created. + +### __unstableSaveResuableBlock + +Returns an action object used in signalling that a temporary reusable blocks have been saved +in order to switch its temporary id with the real id. + +*Parameters* + + * id: Reusable block's id. + * updatedId: Updated block's id. \ No newline at end of file diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index e8759c34d1790..7de4e4f80ad1a 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -584,19 +584,6 @@ before state satisfies the given predicate function. Whether predicate matches for some history. -### getBlockListSettings - -Returns the Block List settings of a block, if any exist. - -*Parameters* - - * state: Editor state. - * clientId: Block client ID. - -*Returns* - -Block settings of the block if set. - ### isPostLocked Returns whether the post is locked. @@ -694,6 +681,18 @@ Return the current block list. Block list. +### isEditorReady + +Is the editor ready + +*Parameters* + + * state: null + +*Returns* + +is Ready. + ## Actions ### setupEditor @@ -741,7 +740,6 @@ Returns an action object used to setup the editor state when first opening an ed *Parameters* * post: Post object. - * blocks: Array of blocks. ### editPost diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index f3e38e0c7b53a..c25a394f01936 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -53,6 +53,7 @@ 'wp-data', 'wp-element', 'wp-i18n', + 'wp-notices', ), 'wp-blocks' => array( 'lodash', diff --git a/package-lock.json b/package-lock.json index 96bef502043dc..fa2faf11ea78c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2350,8 +2350,9 @@ "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", "@wordpress/i18n": "file:packages/i18n", + "@wordpress/notices": "file:packages/notices", "lodash": "^4.17.10", - "redux-optimist": "^1.0.0", + "refx": "^3.0.0", "rememo": "^3.0.0" } }, diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index d4f3cf7f757c7..c115eb96e7d31 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -28,6 +28,7 @@ "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", + "@wordpress/notices": "file:../notices", "lodash": "^4.17.10", "refx": "^3.0.0", "rememo": "^3.0.0" diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 024b0225b3c3e..03a215da73d80 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -3,6 +3,7 @@ */ import '@wordpress/blocks'; +import '@wordpress/notices'; /** * Internal dependencies diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index b8f7179f21b73..1b85ba8cdd41d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -12,7 +12,6 @@ import { keys, isEqual, isEmpty, - overSome, get, } from 'lodash'; @@ -32,22 +31,6 @@ import { } from './defaults'; import { insertAt, moveTo } from './array'; -/** - * Returns a post attribute value, flattening nested rendered content using its - * raw value in place of its original object form. - * - * @param {*} value Original value. - * - * @return {*} Raw value. - */ -export function getPostRawValue( value ) { - if ( value && 'object' === typeof value && 'raw' in value ) { - return value.raw; - } - - return value; -} - /** * Given an array of blocks, returns an object where each key is a nesting * context, the value of which is an array of block client IDs existing within @@ -191,23 +174,6 @@ export function isUpdatingSameBlockAttribute( action, previousAction ) { ); } -/** - * Returns true if, given the currently dispatching action and the previously - * dispatched action, the two actions are editing the same post property, or - * false otherwise. - * - * @param {Object} action Currently dispatching action. - * @param {Object} previousAction Previously dispatched action. - * - * @return {boolean} Whether actions are updating the same post property. - */ -export function isUpdatingSamePostProperty( action, previousAction ) { - return ( - action.type === 'EDIT_POST' && - hasSameKeys( action.edits, previousAction.edits ) - ); -} - /** * Returns true if, given the currently dispatching action and the previously * dispatched action, the two actions are modifying the same property such that @@ -223,10 +189,7 @@ export function shouldOverwriteState( action, previousAction ) { return false; } - return overSome( [ - isUpdatingSameBlockAttribute, - isUpdatingSamePostProperty, - ] )( action, previousAction ); + return isUpdatingSameBlockAttribute( action, previousAction ); } /** diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js new file mode 100644 index 0000000000000..c1d08c75dda50 --- /dev/null +++ b/packages/block-editor/src/store/test/actions.js @@ -0,0 +1,338 @@ +/** + * Internal dependencies + */ +import { + replaceBlocks, + startTyping, + stopTyping, + enterFormattedText, + exitFormattedText, + toggleSelection, + resetBlocks, + updateBlockAttributes, + updateBlock, + selectBlock, + startMultiSelect, + stopMultiSelect, + multiSelect, + clearSelectedBlock, + replaceBlock, + insertBlock, + insertBlocks, + showInsertionPoint, + hideInsertionPoint, + mergeBlocks, + redo, + undo, + removeBlocks, + removeBlock, + toggleBlockMode, + updateBlockListSettings, +} from '../actions'; + +describe( 'actions', () => { + describe( 'resetBlocks', () => { + it( 'should return the RESET_BLOCKS actions', () => { + const blocks = []; + const result = resetBlocks( blocks ); + expect( result ).toEqual( { + type: 'RESET_BLOCKS', + blocks, + } ); + } ); + } ); + + describe( 'updateBlockAttributes', () => { + it( 'should return the UPDATE_BLOCK_ATTRIBUTES action', () => { + const clientId = 'myclientid'; + const attributes = {}; + const result = updateBlockAttributes( clientId, attributes ); + expect( result ).toEqual( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId, + attributes, + } ); + } ); + } ); + + describe( 'updateBlock', () => { + it( 'should return the UPDATE_BLOCK action', () => { + const clientId = 'myclientid'; + const updates = {}; + const result = updateBlock( clientId, updates ); + expect( result ).toEqual( { + type: 'UPDATE_BLOCK', + clientId, + updates, + } ); + } ); + } ); + + describe( 'selectBlock', () => { + it( 'should return the SELECT_BLOCK action', () => { + const clientId = 'myclientid'; + const result = selectBlock( clientId, -1 ); + expect( result ).toEqual( { + type: 'SELECT_BLOCK', + initialPosition: -1, + clientId, + } ); + } ); + } ); + + describe( 'startMultiSelect', () => { + it( 'should return the START_MULTI_SELECT', () => { + expect( startMultiSelect() ).toEqual( { + type: 'START_MULTI_SELECT', + } ); + } ); + } ); + + describe( 'stopMultiSelect', () => { + it( 'should return the Stop_MULTI_SELECT', () => { + expect( stopMultiSelect() ).toEqual( { + type: 'STOP_MULTI_SELECT', + } ); + } ); + } ); + describe( 'multiSelect', () => { + it( 'should return MULTI_SELECT action', () => { + const start = 'start'; + const end = 'end'; + expect( multiSelect( start, end ) ).toEqual( { + type: 'MULTI_SELECT', + start, + end, + } ); + } ); + } ); + + describe( 'clearSelectedBlock', () => { + it( 'should return CLEAR_SELECTED_BLOCK action', () => { + expect( clearSelectedBlock() ).toEqual( { + type: 'CLEAR_SELECTED_BLOCK', + } ); + } ); + } ); + + describe( 'replaceBlock', () => { + it( 'should return the REPLACE_BLOCKS action', () => { + const block = { + clientId: 'ribs', + }; + + expect( replaceBlock( [ 'chicken' ], block ) ).toEqual( { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ block ], + time: expect.any( Number ), + } ); + } ); + } ); + + describe( 'replaceBlocks', () => { + it( 'should return the REPLACE_BLOCKS action', () => { + const blocks = [ { + clientId: 'ribs', + } ]; + + expect( replaceBlocks( [ 'chicken' ], blocks ) ).toEqual( { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks, + time: expect.any( Number ), + } ); + } ); + } ); + + describe( 'insertBlock', () => { + it( 'should return the INSERT_BLOCKS action', () => { + const block = { + clientId: 'ribs', + }; + const index = 5; + expect( insertBlock( block, index, 'testclientid' ) ).toEqual( { + type: 'INSERT_BLOCKS', + blocks: [ block ], + index, + rootClientId: 'testclientid', + time: expect.any( Number ), + updateSelection: true, + } ); + } ); + } ); + + describe( 'insertBlocks', () => { + it( 'should return the INSERT_BLOCKS action', () => { + const blocks = [ { + clientId: 'ribs', + } ]; + const index = 3; + expect( insertBlocks( blocks, index, 'testclientid' ) ).toEqual( { + type: 'INSERT_BLOCKS', + blocks, + index, + rootClientId: 'testclientid', + time: expect.any( Number ), + updateSelection: true, + } ); + } ); + } ); + + describe( 'showInsertionPoint', () => { + it( 'should return the SHOW_INSERTION_POINT action', () => { + expect( showInsertionPoint() ).toEqual( { + type: 'SHOW_INSERTION_POINT', + } ); + } ); + } ); + + describe( 'hideInsertionPoint', () => { + it( 'should return the HIDE_INSERTION_POINT action', () => { + expect( hideInsertionPoint() ).toEqual( { + type: 'HIDE_INSERTION_POINT', + } ); + } ); + } ); + + describe( 'mergeBlocks', () => { + it( 'should return MERGE_BLOCKS action', () => { + const firstBlockClientId = 'blockA'; + const secondBlockClientId = 'blockB'; + expect( mergeBlocks( firstBlockClientId, secondBlockClientId ) ).toEqual( { + type: 'MERGE_BLOCKS', + blocks: [ firstBlockClientId, secondBlockClientId ], + } ); + } ); + } ); + + describe( 'redo', () => { + it( 'should return REDO action', () => { + expect( redo() ).toEqual( { + type: 'REDO', + } ); + } ); + } ); + + describe( 'undo', () => { + it( 'should return UNDO action', () => { + expect( undo() ).toEqual( { + type: 'UNDO', + } ); + } ); + } ); + + describe( 'removeBlocks', () => { + it( 'should return REMOVE_BLOCKS action', () => { + const clientIds = [ 'clientId' ]; + expect( removeBlocks( clientIds ) ).toEqual( { + type: 'REMOVE_BLOCKS', + clientIds, + selectPrevious: true, + } ); + } ); + } ); + + describe( 'removeBlock', () => { + it( 'should return REMOVE_BLOCKS action', () => { + const clientId = 'myclientid'; + expect( removeBlock( clientId ) ).toEqual( { + type: 'REMOVE_BLOCKS', + clientIds: [ + clientId, + ], + selectPrevious: true, + } ); + expect( removeBlock( clientId, false ) ).toEqual( { + type: 'REMOVE_BLOCKS', + clientIds: [ + clientId, + ], + selectPrevious: false, + } ); + } ); + } ); + + describe( 'toggleBlockMode', () => { + it( 'should return TOGGLE_BLOCK_MODE action', () => { + const clientId = 'myclientid'; + expect( toggleBlockMode( clientId ) ).toEqual( { + type: 'TOGGLE_BLOCK_MODE', + clientId, + } ); + } ); + } ); + + describe( 'startTyping', () => { + it( 'should return the START_TYPING action', () => { + expect( startTyping() ).toEqual( { + type: 'START_TYPING', + } ); + } ); + } ); + + describe( 'stopTyping', () => { + it( 'should return the STOP_TYPING action', () => { + expect( stopTyping() ).toEqual( { + type: 'STOP_TYPING', + } ); + } ); + } ); + + describe( 'enterFormattedText', () => { + it( 'should return the ENTER_FORMATTED_TEXT action', () => { + expect( enterFormattedText() ).toEqual( { + type: 'ENTER_FORMATTED_TEXT', + } ); + } ); + } ); + + describe( 'exitFormattedText', () => { + it( 'should return the EXIT_FORMATTED_TEXT action', () => { + expect( exitFormattedText() ).toEqual( { + type: 'EXIT_FORMATTED_TEXT', + } ); + } ); + } ); + + describe( 'toggleSelection', () => { + it( 'should return the TOGGLE_SELECTION action with default value for isSelectionEnabled = true', () => { + expect( toggleSelection() ).toEqual( { + type: 'TOGGLE_SELECTION', + isSelectionEnabled: true, + } ); + } ); + + it( 'should return the TOGGLE_SELECTION action with isSelectionEnabled = true as passed in the argument', () => { + expect( toggleSelection( true ) ).toEqual( { + type: 'TOGGLE_SELECTION', + isSelectionEnabled: true, + } ); + } ); + + it( 'should return the TOGGLE_SELECTION action with isSelectionEnabled = false as passed in the argument', () => { + expect( toggleSelection( false ) ).toEqual( { + type: 'TOGGLE_SELECTION', + isSelectionEnabled: false, + } ); + } ); + } ); + + describe( 'updateBlockListSettings', () => { + it( 'should return the UPDATE_BLOCK_LIST_SETTINGS with undefined settings', () => { + expect( updateBlockListSettings( 'chicken' ) ).toEqual( { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: 'chicken', + settings: undefined, + } ); + } ); + + it( 'should return the UPDATE_BLOCK_LIST_SETTINGS action with the passed settings', () => { + expect( updateBlockListSettings( 'chicken', { chicken: 'ribs' } ) ).toEqual( { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: 'chicken', + settings: { chicken: 'ribs' }, + } ); + } ); + } ); +} ); diff --git a/packages/editor/src/store/test/array.js b/packages/block-editor/src/store/test/array.js similarity index 100% rename from packages/editor/src/store/test/array.js rename to packages/block-editor/src/store/test/array.js diff --git a/packages/block-editor/src/store/test/effects.js b/packages/block-editor/src/store/test/effects.js new file mode 100644 index 0000000000000..6b81becf4e79d --- /dev/null +++ b/packages/block-editor/src/store/test/effects.js @@ -0,0 +1,290 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + getBlockTypes, + unregisterBlockType, + registerBlockType, + createBlock, +} from '@wordpress/blocks'; +import { dispatch as dataDispatch, createRegistry } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import actions, { + updateEditorSettings, + mergeBlocks, + replaceBlocks, + resetBlocks, + selectBlock, + setTemplateValidity, +} from '../actions'; +import effects, { validateBlocksToTemplate } from '../effects'; +import * as selectors from '../selectors'; +import reducer from '../reducer'; +import applyMiddlewares from '../middlewares'; +import '../../'; + +describe( 'effects', () => { + beforeAll( () => { + jest.spyOn( dataDispatch( 'core/notices' ), 'createErrorNotice' ); + jest.spyOn( dataDispatch( 'core/notices' ), 'createSuccessNotice' ); + } ); + + beforeEach( () => { + dataDispatch( 'core/notices' ).createErrorNotice.mockReset(); + dataDispatch( 'core/notices' ).createSuccessNotice.mockReset(); + } ); + + const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; + + describe( '.MERGE_BLOCKS', () => { + const handler = effects.MERGE_BLOCKS; + const defaultGetBlock = selectors.getBlock; + + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + selectors.getBlock = defaultGetBlock; + } ); + + it( 'should only focus the blockA if the blockA has no merge function', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + const blockA = { + clientId: 'chicken', + name: 'core/test-block', + }; + const blockB = { + clientId: 'ribs', + name: 'core/test-block', + }; + selectors.getBlock = ( state, clientId ) => { + return blockA.clientId === clientId ? blockA : blockB; + }; + + const dispatch = jest.fn(); + const getState = () => ( {} ); + handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); + + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken' ) ); + } ); + + it( 'should merge the blocks if blocks of the same type', () => { + registerBlockType( 'core/test-block', { + merge( attributes, attributesToMerge ) { + return { + content: attributes.content + ' ' + attributesToMerge.content, + }; + }, + save: noop, + category: 'common', + title: 'test block', + } ); + const blockA = { + clientId: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken' }, + }; + const blockB = { + clientId: 'ribs', + name: 'core/test-block', + attributes: { content: 'ribs' }, + }; + selectors.getBlock = ( state, clientId ) => { + return blockA.clientId === clientId ? blockA : blockB; + }; + const dispatch = jest.fn(); + const getState = () => ( {} ); + handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); + + expect( dispatch ).toHaveBeenCalledTimes( 2 ); + expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken', -1 ) ); + expect( dispatch ).toHaveBeenCalledWith( { + ...replaceBlocks( [ 'chicken', 'ribs' ], [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken ribs' }, + } ] ), + time: expect.any( Number ), + } ); + } ); + + it( 'should not merge the blocks have different types without transformation', () => { + registerBlockType( 'core/test-block', { + merge( attributes, attributesToMerge ) { + return { + content: attributes.content + ' ' + attributesToMerge.content, + }; + }, + save: noop, + category: 'common', + title: 'test block', + } ); + registerBlockType( 'core/test-block-2', defaultBlockSettings ); + const blockA = { + clientId: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken' }, + }; + const blockB = { + clientId: 'ribs', + name: 'core/test-block2', + attributes: { content: 'ribs' }, + }; + selectors.getBlock = ( state, clientId ) => { + return blockA.clientId === clientId ? blockA : blockB; + }; + const dispatch = jest.fn(); + const getState = () => ( {} ); + handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); + + expect( dispatch ).not.toHaveBeenCalled(); + } ); + + it( 'should transform and merge the blocks', () => { + registerBlockType( 'core/test-block', { + attributes: { + content: { + type: 'string', + }, + }, + merge( attributes, attributesToMerge ) { + return { + content: attributes.content + ' ' + attributesToMerge.content, + }; + }, + save: noop, + category: 'common', + title: 'test block', + } ); + registerBlockType( 'core/test-block-2', { + attributes: { + content: { + type: 'string', + }, + }, + transforms: { + to: [ { + type: 'block', + blocks: [ 'core/test-block' ], + transform: ( { content2 } ) => { + return createBlock( 'core/test-block', { + content: content2, + } ); + }, + } ], + }, + save: noop, + category: 'common', + title: 'test block 2', + } ); + const blockA = { + clientId: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken' }, + }; + const blockB = { + clientId: 'ribs', + name: 'core/test-block-2', + attributes: { content2: 'ribs' }, + }; + selectors.getBlock = ( state, clientId ) => { + return blockA.clientId === clientId ? blockA : blockB; + }; + const dispatch = jest.fn(); + const getState = () => ( {} ); + handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); + + expect( dispatch ).toHaveBeenCalledTimes( 2 ); + // expect( dispatch ).toHaveBeenCalledWith( focusBlock( 'chicken', { offset: -1 } ) ); + expect( dispatch ).toHaveBeenCalledWith( { + ...replaceBlocks( [ 'chicken', 'ribs' ], [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken ribs' }, + } ] ), + time: expect.any( Number ), + } ); + } ); + } ); + + describe( 'validateBlocksToTemplate', () => { + let store; + beforeEach( () => { + store = createRegistry().registerStore( 'test', { + actions, + selectors, + reducer, + } ); + applyMiddlewares( store ); + + registerBlockType( 'core/test-block', defaultBlockSettings ); + } ); + + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'should return undefined if no template assigned', () => { + const result = validateBlocksToTemplate( resetBlocks( [ + createBlock( 'core/test-block' ), + ] ), store ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should return undefined if invalid but unlocked', () => { + store.dispatch( updateEditorSettings( { + template: [ + [ 'core/foo', {} ], + ], + } ) ); + + const result = validateBlocksToTemplate( resetBlocks( [ + createBlock( 'core/test-block' ), + ] ), store ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should return undefined if locked and valid', () => { + store.dispatch( updateEditorSettings( { + template: [ + [ 'core/test-block' ], + ], + templateLock: 'all', + } ) ); + + const result = validateBlocksToTemplate( resetBlocks( [ + createBlock( 'core/test-block' ), + ] ), store ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should return validity set action if invalid on default state', () => { + store.dispatch( updateEditorSettings( { + template: [ + [ 'core/foo' ], + ], + templateLock: 'all', + } ) ); + + const result = validateBlocksToTemplate( resetBlocks( [ + createBlock( 'core/test-block' ), + ] ), store ); + + expect( result ).toEqual( setTemplateValidity( false ) ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js new file mode 100644 index 0000000000000..f6a75d2a0b37c --- /dev/null +++ b/packages/block-editor/src/store/test/reducer.js @@ -0,0 +1,1733 @@ +/** + * External dependencies + */ +import { values, noop } from 'lodash'; +import deepFreeze from 'deep-freeze'; + +/** + * WordPress dependencies + */ +import { + registerBlockType, + unregisterBlockType, + createBlock, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + hasSameKeys, + isUpdatingSameBlockAttribute, + shouldOverwriteState, + editor, + isTyping, + isCaretWithinFormattedText, + blockSelection, + preferences, + blocksMode, + insertionPoint, + template, + blockListSettings, +} 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: 'EDIT_POST', + edits: {}, + }; + const previousAction = { + type: 'EDIT_POST', + edits: {}, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if not updating the same block', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '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', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + bar: 20, + }, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return true if updating the same block attributes', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 20, + }, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( true ); + } ); + } ); + + describe( 'shouldOverwriteState()', () => { + it( 'should return false if no previous action', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = undefined; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if the action types are different', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'REPLACE_BLOCKS', + }; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return true if updating same block attribute', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 20, + }, + }; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( true ); + } ); + } ); + + describe( 'editor()', () => { + beforeAll( () => { + registerBlockType( 'core/test-block', { + save: noop, + edit: noop, + category: 'common', + title: 'test block', + } ); + } ); + + afterAll( () => { + unregisterBlockType( 'core/test-block' ); + } ); + + it( 'should return history (empty edits, blocks) by default', () => { + const state = editor( undefined, {} ); + + expect( state.past ).toEqual( [] ); + expect( state.future ).toEqual( [] ); + expect( state.present.blocks.byClientId ).toEqual( {} ); + expect( state.present.blocks.order ).toEqual( {} ); + } ); + + it( 'should key by reset blocks clientId', () => { + const original = editor( undefined, {} ); + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ { clientId: 'bananas', innerBlocks: [] } ], + } ); + + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'bananas' ); + expect( state.present.blocks.order ).toEqual( { + '': [ 'bananas' ], + bananas: [], + } ); + } ); + + it( 'should key by reset blocks clientId, including inner blocks', () => { + const original = editor( undefined, {} ); + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'bananas', + innerBlocks: [ { clientId: 'apples', innerBlocks: [] } ], + } ], + } ); + + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); + expect( state.present.blocks.order ).toEqual( { + '': [ 'bananas' ], + apples: [], + bananas: [ 'apples' ], + } ); + } ); + + it( 'should insert block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'ribs', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); + expect( values( state.present.blocks.byClientId )[ 1 ].clientId ).toBe( 'ribs' ); + expect( state.present.blocks.order ).toEqual( { + '': [ 'chicken', 'ribs' ], + chicken: [], + ribs: [], + } ); + } ); + + it( 'should replace the block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ { + clientId: 'wings', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( state.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'wings' ); + expect( state.present.blocks.order ).toEqual( { + '': [ 'wings' ], + wings: [], + } ); + } ); + + 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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + + const state = editor( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ nestedBlock.clientId ], + blocks: [ replacementBlock ], + } ); + + expect( state.present.blocks.order ).toEqual( { + '': [ wrapperBlock.clientId ], + [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], + [ replacementBlock.clientId ]: [], + } ); + } ); + + it( 'should replace the block even if the new block clientId is the same', () => { + const originalState = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const replacedState = editor( originalState, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ { + clientId: 'chicken', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( replacedState.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( originalState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/test-block' ); + expect( values( replacedState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( replacedState.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'chicken' ); + expect( replacedState.present.blocks.order ).toEqual( { + '': [ 'chicken' ], + 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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + + const replacedNestedState = editor( originalNestedState, { + type: 'REPLACE_BLOCKS', + clientIds: [ nestedBlock.clientId ], + blocks: [ replacementNestedBlock ], + } ); + + expect( replacedNestedState.present.blocks.order ).toEqual( { + '': [ wrapperBlock.clientId ], + [ wrapperBlock.clientId ]: [ replacementNestedBlock.clientId ], + [ replacementNestedBlock.clientId ]: [], + } ); + + expect( originalNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/test-block' ); + expect( replacedNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/freeform' ); + } ); + + it( 'should update the block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + isValid: false, + innerBlocks: [], + } ], + } ); + const state = editor( deepFreeze( original ), { + type: 'UPDATE_BLOCK', + clientId: 'chicken', + updates: { + attributes: { content: 'ribs' }, + isValid: true, + }, + } ); + + expect( state.present.blocks.byClientId.chicken ).toEqual( { + clientId: 'chicken', + name: 'core/test-block', + isValid: true, + } ); + + expect( state.present.blocks.attributes.chicken ).toEqual( { + content: 'ribs', + } ); + } ); + + it( 'should update the reusable block reference if the temporary id is swapped', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/block', + attributes: { + ref: 'random-clientId', + }, + isValid: false, + innerBlocks: [], + } ], + } ); + + const state = editor( deepFreeze( original ), { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id: 'random-clientId', + updatedId: 3, + } ); + + expect( state.present.blocks.byClientId.chicken ).toEqual( { + clientId: 'chicken', + name: 'core/block', + isValid: false, + } ); + + expect( state.present.blocks.attributes.chicken ).toEqual( { + ref: 3, + } ); + } ); + + it( 'should move the block up', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ 'ribs' ], + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', '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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ movedBlock.clientId ], + rootClientId: wrapperBlock.clientId, + } ); + + expect( state.present.blocks.order ).toEqual( { + '': [ wrapperBlock.clientId ], + [ wrapperBlock.clientId ]: [ movedBlock.clientId, siblingBlock.clientId ], + [ movedBlock.clientId ]: [], + [ siblingBlock.clientId ]: [], + } ); + } ); + + it( 'should move multiple blocks up', () => { + const original = editor( 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 = editor( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ 'ribs', 'veggies' ], + } ); + + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], + rootClientId: wrapperBlock.clientId, + } ); + + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ 'chicken' ], + } ); + + expect( state.present.blocks.order ).toBe( original.present.blocks.order ); + } ); + + it( 'should move the block down', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + clientIds: [ 'chicken' ], + } ); + + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + clientIds: [ movedBlock.clientId ], + rootClientId: wrapperBlock.clientId, + } ); + + expect( state.present.blocks.order ).toEqual( { + '': [ wrapperBlock.clientId ], + [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlock.clientId ], + [ movedBlock.clientId ]: [], + [ siblingBlock.clientId ]: [], + } ); + } ); + + it( 'should move multiple blocks down', () => { + const original = editor( 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 = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + clientIds: [ 'chicken', 'ribs' ], + } ); + + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], + rootClientId: wrapperBlock.clientId, + } ); + + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + clientIds: [ 'ribs' ], + } ); + + expect( state.present.blocks.order ).toBe( original.present.blocks.order ); + } ); + + it( 'should remove the block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ 'chicken' ], + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); + expect( state.present.blocks.byClientId ).toEqual( { + ribs: { + clientId: 'ribs', + name: 'core/test-block', + }, + } ); + expect( state.present.blocks.attributes ).toEqual( { + ribs: {}, + } ); + } ); + + it( 'should remove multiple blocks', () => { + const original = editor( 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 = editor( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ 'chicken', 'veggies' ], + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); + expect( state.present.blocks.order ).not.toHaveProperty( 'veggies' ); + expect( state.present.blocks.byClientId ).toEqual( { + ribs: { + clientId: 'ribs', + name: 'core/test-block', + }, + } ); + expect( state.present.blocks.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 = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ block ], + } ); + + const state = editor( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ block.clientId ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( {} ); + expect( state.present.blocks.order ).toEqual( { + '': [], + } ); + } ); + + it( 'should insert at the specified index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + clientId: 'loquat', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + + const state = editor( original, { + type: 'INSERT_BLOCKS', + index: 1, + blocks: [ { + clientId: 'persimmon', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 3 ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); + } ); + + it( 'should move block to lower index', () => { + const original = editor( 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 = editor( original, { + type: 'MOVE_BLOCK_TO_POSITION', + clientId: 'ribs', + index: 0, + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies' ] ); + } ); + + it( 'should move block to higher index', () => { + const original = editor( 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 = editor( original, { + type: 'MOVE_BLOCK_TO_POSITION', + clientId: 'ribs', + index: 2, + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs' ] ); + } ); + + it( 'should not move block if passed same index', () => { + const original = editor( 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 = editor( original, { + type: 'MOVE_BLOCK_TO_POSITION', + clientId: 'ribs', + index: 1, + } ); + + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies' ] ); + } ); + + describe( 'blocks', () => { + it( 'should not reset any blocks that are not in the post', () => { + const actions = [ + { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block1', + innerBlocks: [ + { clientId: 'block11', innerBlocks: [] }, + { clientId: 'block12', innerBlocks: [] }, + ], + }, + ], + }, + { + type: 'RECEIVE_BLOCKS', + blocks: [ + { + clientId: 'block2', + innerBlocks: [ + { clientId: 'block21', innerBlocks: [] }, + { clientId: 'block22', innerBlocks: [] }, + ], + }, + ], + }, + ]; + const original = deepFreeze( actions.reduce( editor, undefined ) ); + + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block3', + innerBlocks: [ + { clientId: 'block31', innerBlocks: [] }, + { clientId: 'block32', innerBlocks: [] }, + ], + }, + ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( { + block2: { clientId: 'block2' }, + block21: { clientId: 'block21' }, + block22: { clientId: 'block22' }, + block3: { clientId: 'block3' }, + block31: { clientId: 'block31' }, + block32: { clientId: 'block32' }, + } ); + } ); + + describe( 'byClientId', () => { + it( 'should ignore updates to non-existent block', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); + + expect( state.present.blocks.byClientId ).toBe( original.present.blocks.byClientId ); + } ); + + it( 'should return with same reference if no changes in updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: { + updated: true, + }, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); + + expect( state.present.blocks.byClientId ).toBe( state.present.blocks.byClientId ); + } ); + } ); + + describe( 'attributes', () => { + it( 'should return with attribute block updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: {}, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); + + expect( state.present.blocks.attributes.kumquat.updated ).toBe( true ); + } ); + + it( 'should accumulate attribute block updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: { + updated: true, + }, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + moreUpdated: true, + }, + } ); + + expect( state.present.blocks.attributes.kumquat ).toEqual( { + updated: true, + moreUpdated: true, + } ); + } ); + + it( 'should ignore updates to non-existent block', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); + + expect( state.present.blocks.attributes ).toBe( original.present.blocks.attributes ); + } ); + + it( 'should return with same reference if no changes in updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: { + updated: true, + }, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); + + expect( state.present.blocks.attributes ).toBe( state.present.blocks.attributes ); + } ); + } ); + } ); + + describe( 'withHistory', () => { + it( 'should overwrite present history if updating same attributes', () => { + let state; + + state = editor( state, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: {}, + innerBlocks: [], + } ], + } ); + + expect( state.past ).toHaveLength( 1 ); + + state = editor( state, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + test: 1, + }, + } ); + + state = editor( state, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + test: 2, + }, + } ); + + expect( state.past ).toHaveLength( 2 ); + } ); + + it( 'should not overwrite present history if updating different attributes', () => { + let state; + + state = editor( state, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: {}, + innerBlocks: [], + } ], + } ); + + expect( state.past ).toHaveLength( 1 ); + + state = editor( state, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + test: 1, + }, + } ); + + state = editor( state, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + other: 1, + }, + } ); + + expect( state.past ).toHaveLength( 3 ); + } ); + } ); + } ); + + 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( 'isCaretWithinFormattedText()', () => { + it( 'should set the flag to true', () => { + const state = isCaretWithinFormattedText( false, { + type: 'ENTER_FORMATTED_TEXT', + } ); + + expect( state ).toBe( true ); + } ); + + it( 'should set the flag to false', () => { + const state = isCaretWithinFormattedText( true, { + type: 'EXIT_FORMATTED_TEXT', + } ); + + expect( state ).toBe( false ); + } ); + } ); + + describe( 'blockSelection()', () => { + it( 'should return with block clientId as selected', () => { + const state = blockSelection( undefined, { + type: 'SELECT_BLOCK', + clientId: 'kumquat', + initialPosition: -1, + } ); + + expect( state ).toEqual( { + start: 'kumquat', + end: 'kumquat', + initialPosition: -1, + isMultiSelecting: false, + isEnabled: true, + } ); + } ); + + it( 'should set multi selection', () => { + const original = deepFreeze( { isMultiSelecting: false } ); + const state = blockSelection( original, { + type: 'MULTI_SELECT', + start: 'ribs', + end: 'chicken', + } ); + + expect( state ).toEqual( { + start: 'ribs', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should set continuous multi selection', () => { + const original = deepFreeze( { isMultiSelecting: true } ); + const state = blockSelection( original, { + type: 'MULTI_SELECT', + start: 'ribs', + end: 'chicken', + } ); + + expect( state ).toEqual( { + start: 'ribs', + end: 'chicken', + initialPosition: null, + isMultiSelecting: true, + } ); + } ); + + it( 'should start multi selection', () => { + const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: false } ); + const state = blockSelection( original, { + type: 'START_MULTI_SELECT', + } ); + + expect( state ).toEqual( { + start: 'ribs', + end: 'ribs', + initialPosition: null, + isMultiSelecting: true, + } ); + } ); + + it( 'should return same reference if already multi-selecting', () => { + const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: true } ); + const state = blockSelection( original, { + type: 'START_MULTI_SELECT', + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should end multi selection with selection', () => { + const original = deepFreeze( { start: 'ribs', end: 'chicken', isMultiSelecting: true } ); + const state = blockSelection( original, { + type: 'STOP_MULTI_SELECT', + } ); + + expect( state ).toEqual( { + start: 'ribs', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should return same reference if already ended multi-selecting', () => { + const original = deepFreeze( { start: 'ribs', end: 'chicken', isMultiSelecting: false } ); + const state = blockSelection( original, { + type: 'STOP_MULTI_SELECT', + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should end multi selection without selection', () => { + const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: true } ); + const state = blockSelection( original, { + type: 'STOP_MULTI_SELECT', + } ); + + expect( state ).toEqual( { + start: 'ribs', + end: 'ribs', + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should not update the state if the block is already selected', () => { + const original = deepFreeze( { start: 'ribs', end: 'ribs' } ); + + const state1 = blockSelection( original, { + type: 'SELECT_BLOCK', + clientId: 'ribs', + } ); + + expect( state1 ).toBe( original ); + } ); + + it( 'should unset multi selection', () => { + const original = deepFreeze( { start: 'ribs', end: 'chicken' } ); + + const state1 = blockSelection( original, { + type: 'CLEAR_SELECTED_BLOCK', + } ); + + expect( state1 ).toEqual( { + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should return same reference if clearing selection but no selection', () => { + const original = deepFreeze( { start: null, end: null, isMultiSelecting: false } ); + + const state1 = blockSelection( original, { + type: 'CLEAR_SELECTED_BLOCK', + } ); + + expect( state1 ).toBe( original ); + } ); + + it( 'should select inserted block', () => { + const original = deepFreeze( { start: 'ribs', end: 'chicken' } ); + + const state3 = blockSelection( original, { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'ribs', + name: 'core/freeform', + } ], + updateSelection: true, + } ); + + expect( state3 ).toEqual( { + start: 'ribs', + end: 'ribs', + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should not select inserted block if updateSelection flag is false', () => { + const original = deepFreeze( { start: 'a', end: 'b' } ); + + const state3 = blockSelection( original, { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'ribs', + name: 'core/freeform', + } ], + updateSelection: false, + } ); + + expect( state3 ).toEqual( { + start: 'a', + end: 'b', + } ); + } ); + + it( 'should not update the state if the block moved is already selected', () => { + const original = deepFreeze( { start: 'ribs', end: 'ribs' } ); + const state = blockSelection( original, { + type: 'MOVE_BLOCKS_UP', + clientIds: [ 'ribs' ], + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should replace the selected block', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ { + clientId: 'wings', + name: 'core/freeform', + } ], + } ); + + expect( state ).toEqual( { + start: 'wings', + end: 'wings', + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should not replace the selected block if we keep it when replacing blocks', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ + { + clientId: 'chicken', + name: 'core/freeform', + }, + { + clientId: 'wings', + name: 'core/freeform', + } ], + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should reset if replacing with empty set', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [], + } ); + + expect( state ).toEqual( { + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should keep the selected block', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'ribs' ], + blocks: [ { + clientId: 'wings', + name: 'core/freeform', + } ], + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should remove the selection if we are removing the selected block', () => { + const original = deepFreeze( { + start: 'chicken', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + const state = blockSelection( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ 'chicken' ], + } ); + + expect( state ).toEqual( { + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should keep the selection if we are not removing the selected block', () => { + const original = deepFreeze( { + start: 'chicken', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + const state = blockSelection( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ 'ribs' ], + } ); + + expect( state ).toBe( original ); + } ); + } ); + + describe( 'preferences()', () => { + it( 'should apply all defaults', () => { + const state = preferences( undefined, {} ); + + expect( state ).toEqual( { + insertUsage: {}, + } ); + } ); + it( 'should record recently used blocks', () => { + const state = preferences( deepFreeze( { insertUsage: {} } ), { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'bacon', + name: 'core-embed/twitter', + } ], + time: 123456, + } ); + + expect( state ).toEqual( { + insertUsage: { + 'core-embed/twitter': { + time: 123456, + count: 1, + insert: { name: 'core-embed/twitter' }, + }, + }, + } ); + + const twoRecentBlocks = preferences( deepFreeze( { + insertUsage: { + 'core-embed/twitter': { + time: 123456, + count: 1, + insert: { name: 'core-embed/twitter' }, + }, + }, + } ), { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'eggs', + name: 'core-embed/twitter', + }, { + clientId: 'bacon', + name: 'core/block', + attributes: { ref: 123 }, + } ], + time: 123457, + } ); + + expect( twoRecentBlocks ).toEqual( { + insertUsage: { + 'core-embed/twitter': { + time: 123457, + count: 2, + insert: { name: 'core-embed/twitter' }, + }, + 'core/block/123': { + time: 123457, + count: 1, + insert: { name: 'core/block', ref: 123 }, + }, + }, + } ); + } ); + } ); + + describe( 'blocksMode', () => { + it( 'should set mode to html if not set', () => { + const action = { + type: 'TOGGLE_BLOCK_MODE', + clientId: 'chicken', + }; + const value = blocksMode( deepFreeze( {} ), action ); + + expect( value ).toEqual( { chicken: 'html' } ); + } ); + + it( 'should toggle mode to visual if set as html', () => { + const action = { + type: 'TOGGLE_BLOCK_MODE', + clientId: 'chicken', + }; + const value = blocksMode( deepFreeze( { chicken: 'html' } ), action ); + + expect( value ).toEqual( { chicken: 'visual' } ); + } ); + } ); + + describe( 'template', () => { + it( 'should default to visible', () => { + const state = template( undefined, {} ); + + expect( state ).toEqual( { isValid: true } ); + } ); + + it( 'should reset the validity flag', () => { + const original = deepFreeze( { isValid: false, template: [] } ); + const state = template( original, { + type: 'SET_TEMPLATE_VALIDITY', + isValid: true, + } ); + + expect( state ).toEqual( { isValid: true, template: [] } ); + } ); + } ); + + describe( 'blockListSettings', () => { + it( 'should add new settings', () => { + const original = deepFreeze( {} ); + + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + settings: { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + + expect( state ).toEqual( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + } ); + + it( 'should return same reference if updated as the same', () => { + const original = deepFreeze( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + settings: { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should return same reference if updated settings not assigned and id not exists', () => { + const original = deepFreeze( {} ); + + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should update the settings of a block', () => { + const original = deepFreeze( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + allowedBlocks: true, + }, + } ); + + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + settings: { + allowedBlocks: [ 'core/list' ], + }, + } ); + + expect( state ).toEqual( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/list' ], + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + allowedBlocks: true, + }, + } ); + } ); + + it( 'should remove existing settings if updated settings not assigned', () => { + const original = deepFreeze( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + } ); + + expect( state ).toEqual( {} ); + } ); + + it( 'should remove the settings of a block when it is replaced', () => { + const original = deepFreeze( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + allowedBlocks: true, + }, + } ); + + const state = blockListSettings( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + } ); + + expect( state ).toEqual( { + '9db792c6-a25a-495d-adbd-97d56a4c4189': { + allowedBlocks: [ 'core/paragraph' ], + }, + } ); + } ); + + it( 'should remove the settings of a block when it is removed', () => { + const original = deepFreeze( { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + allowedBlocks: true, + }, + } ); + + const state = blockListSettings( original, { + type: 'REMOVE_BLOCKS', + clientIds: [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + } ); + + expect( state ).toEqual( {} ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js new file mode 100644 index 0000000000000..b4baa2a69d4dd --- /dev/null +++ b/packages/block-editor/src/store/test/selectors.js @@ -0,0 +1,2815 @@ +/** + * External dependencies + */ +import { filter } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + registerBlockType, + unregisterBlockType, + setFreeformContentHandlerName, +} from '@wordpress/blocks'; +import { RawHTML } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as selectors from '../selectors'; + +const { + hasEditorUndo, + hasEditorRedo, + getBlockDependantsCacheBust, + getBlockName, + getBlock, + getBlocks, + getBlockCount, + getClientIdsWithDescendants, + getClientIdsOfDescendants, + hasSelectedBlock, + getSelectedBlock, + getSelectedBlockClientId, + getBlockRootClientId, + getBlockHierarchyRootClientId, + getGlobalBlockCount, + getMultiSelectedBlockClientIds, + getMultiSelectedBlocks, + getMultiSelectedBlocksStartClientId, + getMultiSelectedBlocksEndClientId, + getBlockOrder, + getBlockIndex, + getPreviousBlockClientId, + getNextBlockClientId, + isBlockSelected, + hasSelectedInnerBlock, + isBlockWithinSelection, + hasMultiSelection, + isBlockMultiSelected, + isFirstMultiSelectedBlock, + getBlockMode, + isTyping, + isCaretWithinFormattedText, + getBlockInsertionPoint, + isBlockInsertionPointVisible, + isSelectionEnabled, + canInsertBlockType, + getInserterItems, + isValidTemplate, + getTemplate, + getTemplateLock, + getBlockListSettings, + INSERTER_UTILITY_HIGH, + INSERTER_UTILITY_MEDIUM, + INSERTER_UTILITY_LOW, +} = selectors; + +describe( 'selectors', () => { + let cachedSelectors; + + beforeAll( () => { + cachedSelectors = filter( selectors, ( selector ) => selector.clear ); + } ); + + 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: 'formatting', + title: 'Test Block A', + icon: 'test', + keywords: [ 'testing' ], + } ); + + registerBlockType( 'core/test-block-b', { + save: ( props ) => props.attributes.text, + category: 'common', + title: 'Test Block B', + icon: 'test', + keywords: [ 'testing' ], + supports: { + multiple: false, + }, + } ); + + registerBlockType( 'core/test-block-c', { + save: ( props ) => props.attributes.text, + category: 'common', + title: 'Test Block C', + icon: 'test', + keywords: [ 'testing' ], + parent: [ 'core/test-block-b' ], + } ); + + registerBlockType( 'core/test-freeform', { + save: ( props ) => { props.attributes.content }, + category: 'common', + title: 'Test Freeform Content Handler', + icon: 'test', + attributes: { + content: { + type: 'string', + }, + }, + } ); + + setFreeformContentHandlerName( 'core/test-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/test-freeform' ); + + setFreeformContentHandlerName( undefined ); + } ); + + describe( 'hasEditorUndo', () => { + it( 'should return true when the past history is not empty', () => { + const state = { + editor: { + past: [ + {}, + ], + }, + }; + + expect( hasEditorUndo( state ) ).toBe( true ); + } ); + + it( 'should return false when the past history is empty', () => { + const state = { + editor: { + past: [], + }, + }; + + expect( hasEditorUndo( state ) ).toBe( false ); + } ); + } ); + + describe( 'hasEditorRedo', () => { + it( 'should return true when the future history is not empty', () => { + const state = { + editor: { + future: [ + {}, + ], + }, + }; + + expect( hasEditorRedo( state ) ).toBe( true ); + } ); + + it( 'should return false when the future history is empty', () => { + const state = { + editor: { + future: [], + }, + }; + + expect( hasEditorRedo( state ) ).toBe( false ); + } ); + } ); + + describe( 'getBlockDependantsCacheBust', () => { + const rootBlock = { clientId: 123, name: 'core/paragraph' }; + const rootBlockAttributes = {}; + const rootOrder = [ 123 ]; + + it( 'returns an unchanging reference', () => { + const rootBlockOrder = []; + + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + }, + attributes: { + 123: rootBlockAttributes, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const nextState = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + }, + attributes: { + 123: rootBlockAttributes, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( + getBlockDependantsCacheBust( state, 123 ) + ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + } ); + + it( 'returns a new reference on added inner block', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + }, + attributes: { + 123: rootBlockAttributes, + }, + order: { + '': rootOrder, + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const nextState = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph' }, + }, + attributes: { + 123: rootBlockAttributes, + 456: {}, + }, + order: { + '': rootOrder, + 123: [ 456 ], + 456: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( + getBlockDependantsCacheBust( state, 123 ) + ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + } ); + + it( 'returns an unchanging reference on unchanging inner block', () => { + const rootBlockOrder = [ 456 ]; + const childBlock = { clientId: 456, name: 'core/paragraph' }; + const childBlockAttributes = {}; + const childBlockOrder = []; + + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + }, + attributes: { + 123: rootBlockAttributes, + 456: childBlockAttributes, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const nextState = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + }, + attributes: { + 123: rootBlockAttributes, + 456: childBlockAttributes, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( + getBlockDependantsCacheBust( state, 123 ) + ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + } ); + + it( 'returns a new reference on updated inner block', () => { + const rootBlockOrder = [ 456 ]; + const childBlockOrder = []; + + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph' }, + }, + attributes: { + 123: rootBlockAttributes, + 456: {}, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const nextState = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph' }, + }, + attributes: { + 123: rootBlockAttributes, + 456: { content: [ 'foo' ] }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( + getBlockDependantsCacheBust( state, 123 ) + ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + } ); + + it( 'returns a new reference on updated grandchild inner block', () => { + const rootBlockOrder = [ 456 ]; + const childBlock = { clientId: 456, name: 'core/paragraph' }; + const childBlockAttributes = {}; + const childBlockOrder = [ 789 ]; + const grandChildBlockOrder = []; + + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + 789: { clientId: 789, name: 'core/paragraph' }, + }, + attributes: { + 123: rootBlockAttributes, + 456: childBlockAttributes, + 789: {}, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + 789: grandChildBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const nextState = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + 789: { clientId: 789, name: 'core/paragraph' }, + }, + attributes: { + 123: rootBlockAttributes, + 456: childBlockAttributes, + 789: { content: [ 'foo' ] }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + 789: grandChildBlockOrder, + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( + getBlockDependantsCacheBust( state, 123 ) + ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + } ); + } ); + + describe( 'getBlockName', () => { + it( 'returns null if no block by clientId', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( null ); + } ); + + it( 'returns block name', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + }, + }, + attributes: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, + }, + order: { + '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( 'core/paragraph' ); + } ); + } ); + + describe( 'getBlock', () => { + it( 'should return the block', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 123: {}, + }, + order: { + '': [ 123 ], + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( getBlock( state, 123 ) ).toEqual( { + clientId: 123, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [], + } ); + } ); + + it( 'should return null if the block is not present in state', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( getBlock( state, 123 ) ).toBe( null ); + } ); + + it( 'should include inner blocks', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/paragraph' }, + 456: { clientId: 456, name: 'core/paragraph' }, + }, + attributes: { + 123: {}, + 456: {}, + }, + order: { + '': [ 123 ], + 123: [ 456 ], + 456: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( getBlock( state, 123 ) ).toEqual( { + clientId: 123, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [ { + clientId: 456, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [], + } ], + } ); + } ); + + it( 'should merge meta attributes for the block', () => { + registerBlockType( 'core/meta-block', { + save: ( props ) => props.attributes.text, + category: 'common', + title: 'test block', + attributes: { + foo: { + type: 'string', + source: 'meta', + meta: 'foo', + }, + }, + } ); + + const state = { + settings: { + __experimentalMetaSource: { + value: { + foo: 'bar', + }, + }, + }, + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/meta-block' }, + }, + attributes: { + 123: {}, + }, + order: { + '': [ 123 ], + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( getBlock( state, 123 ) ).toEqual( { + clientId: 123, + name: 'core/meta-block', + attributes: { + foo: 'bar', + }, + innerBlocks: [], + } ); + + unregisterBlockType( 'core/meta-block' ); + } ); + } ); + + describe( 'getBlocks', () => { + it( 'should return the ordered blocks', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading' }, + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 23: {}, + 123: {}, + }, + order: { + '': [ 123, 23 ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + + expect( getBlocks( state ) ).toEqual( [ + { clientId: 123, name: 'core/paragraph', attributes: {}, innerBlocks: [] }, + { clientId: 23, name: 'core/heading', attributes: {}, innerBlocks: [] }, + ] ); + } ); + } ); + + describe( 'getClientIdsOfDescendants', () => { + it( 'should return the ids of any descendants, given an array of clientIds', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + '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: { + '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: { + '': [ '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' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + expect( getClientIdsOfDescendants( state, [ 'uuid-10' ] ) ).toEqual( [ + 'uuid-12', + 'uuid-14', + 'uuid-16', + 'uuid-18', + 'uuid-24', + 'uuid-26', + 'uuid-28', + 'uuid-30', + ] ); + } ); + } ); + + describe( 'getClientIdsWithDescendants', () => { + it( 'should return the ids for top-level blocks and their descendants of any depth (for nested blocks).', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + '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: { + '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: { + '': [ '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' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + }; + expect( getClientIdsWithDescendants( state ) ).toEqual( [ + 'uuid-6', + 'uuid-8', + 'uuid-10', + 'uuid-22', + 'uuid-12', + 'uuid-14', + 'uuid-16', + 'uuid-18', + 'uuid-24', + 'uuid-26', + 'uuid-28', + 'uuid-30', + ] ); + } ); + } ); + + describe( 'getBlockCount', () => { + it( 'should return the number of top-level blocks in the post', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading' }, + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 23: {}, + 123: {}, + }, + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getBlockCount( state ) ).toBe( 2 ); + } ); + + it( 'should return the number of blocks in a nested context', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/columns' }, + 456: { clientId: 456, name: 'core/paragraph' }, + 789: { clientId: 789, name: 'core/paragraph' }, + }, + attributes: { + 123: {}, + 456: {}, + 789: {}, + }, + order: { + '': [ 123 ], + 123: [ 456, 789 ], + }, + }, + }, + }, + }; + + expect( getBlockCount( state, '123' ) ).toBe( 2 ); + } ); + } ); + + describe( 'hasSelectedBlock', () => { + it( 'should return false if no selection', () => { + const state = { + blockSelection: { + start: null, + end: null, + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( false ); + } ); + + it( 'should return false if multi-selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: '9db792c6-a25a-495d-adbd-97d56a4c4189', + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( false ); + } ); + + it( 'should return true if singular selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( true ); + } ); + } ); + + describe( 'getGlobalBlockCount', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/heading' }, + 456: { clientId: 456, name: 'core/paragraph' }, + 789: { clientId: 789, name: 'core/paragraph' }, + }, + attributes: { + 123: {}, + 456: {}, + 789: {}, + }, + order: { + '': [ 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 = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + }, + }, + }; + expect( getGlobalBlockCount( emptyState ) ).toBe( 0 ); + expect( getGlobalBlockCount( emptyState, 'core/heading' ) ).toBe( 0 ); + } ); + } ); + + describe( 'getSelectedBlockClientId', () => { + it( 'should return null if no block is selected', () => { + const state = { + blockSelection: { start: null, end: null }, + }; + + expect( getSelectedBlockClientId( state ) ).toBe( null ); + } ); + + it( 'should return null if there is multi selection', () => { + const state = { + blockSelection: { start: 23, end: 123 }, + }; + + expect( getSelectedBlockClientId( state ) ).toBe( null ); + } ); + + it( 'should return the selected block ClientId', () => { + const state = { + editor: { present: { blocks: { byClientId: { 23: { name: 'fake block' } } } } }, + blockSelection: { start: 23, end: 23 }, + }; + + expect( getSelectedBlockClientId( state ) ).toEqual( 23 ); + } ); + } ); + + describe( 'getSelectedBlock', () => { + it( 'should return null if no block is selected', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading' }, + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 23: {}, + 123: {}, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + blockSelection: { start: null, end: null }, + }; + + expect( getSelectedBlock( state ) ).toBe( null ); + } ); + + it( 'should return null if there is multi selection', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading' }, + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 23: {}, + 123: {}, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + blockSelection: { start: 23, end: 123 }, + }; + + expect( getSelectedBlock( state ) ).toBe( null ); + } ); + + it( 'should return the selected block', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading' }, + 123: { clientId: 123, name: 'core/paragraph' }, + }, + attributes: { + 23: {}, + 123: {}, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + blockSelection: { start: 23, end: 23 }, + }; + + expect( getSelectedBlock( state ) ).toEqual( { + clientId: 23, + name: 'core/heading', + attributes: {}, + innerBlocks: [], + } ); + } ); + } ); + + describe( 'getBlockRootClientId', () => { + it( 'should return null if the block does not exist', () => { + const state = { + editor: { + present: { + blocks: { + order: {}, + }, + }, + }, + }; + + expect( getBlockRootClientId( state, 56 ) ).toBeNull(); + } ); + + it( 'should return root ClientId relative the block ClientId', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getBlockRootClientId( state, 56 ) ).toBe( '123' ); + } ); + } ); + + describe( 'getBlockHierarchyRootClientId', () => { + it( 'should return the given block if the block has no parents', () => { + const state = { + editor: { + present: { + blocks: { + order: {}, + }, + }, + }, + }; + + expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( 56 ); + } ); + + it( 'should return root ClientId relative the block ClientId', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( '123' ); + } ); + + it( 'should return the top level root ClientId relative the block ClientId', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ '123', '23' ], + 123: [ '456', '56' ], + 56: [ '12' ], + }, + }, + }, + }, + }; + + expect( getBlockHierarchyRootClientId( state, '12' ) ).toBe( '123' ); + } ); + } ); + + describe( 'getMultiSelectedBlockClientIds', () => { + it( 'should return empty if there is no multi selection', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + blockSelection: { start: null, end: null }, + }; + + expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [] ); + } ); + + it( 'should return selected block clientIds if there is multi selection', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + blockSelection: { start: 2, end: 4 }, + }; + + expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ 4, 3, 2 ] ); + } ); + + it( 'should return selected block clientIds if there is multi selection (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + 4: [ 9, 8, 7, 6 ], + }, + }, + }, + }, + blockSelection: { start: 7, end: 9 }, + }; + + expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ 9, 8, 7 ] ); + } ); + } ); + + describe( 'getMultiSelectedBlocks', () => { + it( 'should return the same reference on subsequent invocations of empty selection', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + blockSelection: { start: null, end: null }, + currentPost: {}, + }; + + expect( + getMultiSelectedBlocks( state ) + ).toBe( getMultiSelectedBlocks( state ) ); + } ); + } ); + + describe( 'getMultiSelectedBlocksStartClientId', () => { + it( 'returns null if there is no multi selection', () => { + const state = { + blockSelection: { start: null, end: null }, + }; + + expect( getMultiSelectedBlocksStartClientId( state ) ).toBeNull(); + } ); + + it( 'returns multi selection start', () => { + const state = { + blockSelection: { start: 2, end: 4 }, + }; + + expect( getMultiSelectedBlocksStartClientId( state ) ).toBe( 2 ); + } ); + } ); + + describe( 'getMultiSelectedBlocksEndClientId', () => { + it( 'returns null if there is no multi selection', () => { + const state = { + blockSelection: { start: null, end: null }, + }; + + expect( getMultiSelectedBlocksEndClientId( state ) ).toBeNull(); + } ); + + it( 'returns multi selection end', () => { + const state = { + blockSelection: { start: 2, end: 4 }, + }; + + expect( getMultiSelectedBlocksEndClientId( state ) ).toBe( 4 ); + } ); + } ); + + describe( 'getBlockOrder', () => { + it( 'should return the ordered block ClientIds of top-level blocks by default', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getBlockOrder( state ) ).toEqual( [ 123, 23 ] ); + } ); + + it( 'should return the ordered block ClientIds at a specified rootClientId', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456 ], + }, + }, + }, + }, + }; + + expect( getBlockOrder( state, '123' ) ).toEqual( [ 456 ] ); + } ); + } ); + + describe( 'getBlockIndex', () => { + it( 'should return the block order', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getBlockIndex( state, 23 ) ).toBe( 1 ); + } ); + + it( 'should return the block order (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getBlockIndex( state, 56, '123' ) ).toBe( 1 ); + } ); + } ); + + describe( 'getPreviousBlockClientId', () => { + it( 'should return the previous block', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getPreviousBlockClientId( state, 23 ) ).toEqual( 123 ); + } ); + + it( 'should return the previous block (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getPreviousBlockClientId( state, 56, '123' ) ).toEqual( 456 ); + } ); + + it( 'should return null for the first block', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getPreviousBlockClientId( state, 123 ) ).toBeNull(); + } ); + + it( 'should return null for the first block (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getPreviousBlockClientId( state, 456, '123' ) ).toBeNull(); + } ); + } ); + + describe( 'getNextBlockClientId', () => { + it( 'should return the following block', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getNextBlockClientId( state, 123 ) ).toEqual( 23 ); + } ); + + it( 'should return the following block (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getNextBlockClientId( state, 456, '123' ) ).toEqual( 56 ); + } ); + + it( 'should return null for the last block', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + }, + }, + }, + }, + }; + + expect( getNextBlockClientId( state, 23 ) ).toBeNull(); + } ); + + it( 'should return null for the last block (nested context)', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }, + }; + + expect( getNextBlockClientId( state, 56, '123' ) ).toBeNull(); + } ); + } ); + + describe( 'isBlockSelected', () => { + it( 'should return true if the block is selected', () => { + const state = { + blockSelection: { start: 123, end: 123 }, + }; + + expect( isBlockSelected( state, 123 ) ).toBe( true ); + } ); + + it( 'should return false if a multi-selection range exists', () => { + const state = { + blockSelection: { start: 123, end: 124 }, + }; + + expect( isBlockSelected( state, 123 ) ).toBe( false ); + } ); + + it( 'should return false if the block is not selected', () => { + const state = { + blockSelection: { start: null, end: null }, + }; + + 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 = { + blockSelection: { start: 5, end: 5 }, + editor: { + present: { + blocks: { + order: { + 4: [ 3, 2, 1 ], + }, + }, + }, + }, + }; + + expect( hasSelectedInnerBlock( state, 4 ) ).toBe( false ); + } ); + + it( 'should return true if the selected block is a child of the given ClientId', () => { + const state = { + blockSelection: { start: 3, end: 3 }, + editor: { + present: { + blocks: { + order: { + 4: [ 3, 2, 1 ], + }, + }, + }, + }, + }; + + 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 = { + editor: { + present: { + blocks: { + order: { + 6: [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + blockSelection: { start: 2, end: 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 = { + editor: { + present: { + blocks: { + order: { + 3: [ 2, 1 ], + 6: [ 5, 4 ], + }, + }, + }, + }, + blockSelection: { start: 5, end: 4 }, + }; + expect( hasSelectedInnerBlock( state, 3 ) ).toBe( false ); + } ); + } ); + + describe( 'isBlockWithinSelection', () => { + it( 'should return true if the block is selected but not the last', () => { + const state = { + blockSelection: { start: 5, end: 3 }, + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + }; + + expect( isBlockWithinSelection( state, 4 ) ).toBe( true ); + } ); + + it( 'should return false if the block is the last selected', () => { + const state = { + blockSelection: { start: 5, end: 3 }, + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + }; + + expect( isBlockWithinSelection( state, 3 ) ).toBe( false ); + } ); + + it( 'should return false if the block is not selected', () => { + const state = { + blockSelection: { start: 5, end: 3 }, + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + }; + + expect( isBlockWithinSelection( state, 2 ) ).toBe( false ); + } ); + + it( 'should return false if there is no selection', () => { + const state = { + blockSelection: {}, + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + }; + + expect( isBlockWithinSelection( state, 4 ) ).toBe( false ); + } ); + } ); + + describe( 'hasMultiSelection', () => { + it( 'should return false if no selection', () => { + const state = { + blockSelection: { + start: null, + end: null, + }, + }; + + expect( hasMultiSelection( state ) ).toBe( false ); + } ); + + it( 'should return false if singular selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + }, + }; + + expect( hasMultiSelection( state ) ).toBe( false ); + } ); + + it( 'should return true if multi-selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: '9db792c6-a25a-495d-adbd-97d56a4c4189', + }, + }; + + expect( hasMultiSelection( state ) ).toBe( true ); + } ); + } ); + + describe( 'isBlockMultiSelected', () => { + const state = { + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + blockSelection: { start: 2, end: 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 = { + editor: { + present: { + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, + }, + }, + }, + blockSelection: { start: 2, end: 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( 'isCaretWithinFormattedText', () => { + it( 'returns true if the isCaretWithinFormattedText state is also true', () => { + const state = { + isCaretWithinFormattedText: true, + }; + + expect( isCaretWithinFormattedText( state ) ).toBe( true ); + } ); + + it( 'returns false if the isCaretWithinFormattedText state is also false', () => { + const state = { + isCaretWithinFormattedText: false, + }; + + expect( isCaretWithinFormattedText( state ) ).toBe( false ); + } ); + } ); + + describe( 'isSelectionEnabled', () => { + it( 'should return true if selection is enable', () => { + const state = { + blockSelection: { + isEnabled: true, + }, + }; + + expect( isSelectionEnabled( state ) ).toBe( true ); + } ); + + it( 'should return false if selection is disabled', () => { + const state = { + blockSelection: { + isEnabled: false, + }, + }; + + expect( isSelectionEnabled( state ) ).toBe( false ); + } ); + } ); + + describe( 'getBlockInsertionPoint', () => { + it( 'should return the explicitly assigned insertion point', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId2', + end: 'clientId2', + }, + editor: { + present: { + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + attributes: { + clientId1: {}, + clientId2: {}, + }, + order: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + insertionPoint: { + rootClientId: undefined, + index: 0, + }, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 0, + } ); + } ); + + it( 'should return an object for the selected block', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId1', + end: 'clientId1', + }, + editor: { + present: { + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + }, + attributes: { + clientId1: {}, + }, + order: { + '': [ 'clientId1' ], + clientId1: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + insertionPoint: null, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 1, + } ); + } ); + + it( 'should return an object for the nested selected block', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId2', + end: 'clientId2', + }, + editor: { + present: { + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + attributes: { + clientId1: {}, + clientId2: {}, + }, + order: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + insertionPoint: null, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: 'clientId1', + index: 1, + } ); + } ); + + it( 'should return an object for the last multi selected clientId', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId1', + end: 'clientId2', + }, + editor: { + present: { + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + attributes: { + clientId1: {}, + clientId2: {}, + }, + order: { + '': [ 'clientId1', 'clientId2' ], + clientId1: [], + clientId2: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + insertionPoint: null, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 2, + } ); + } ); + + it( 'should return an object for the last block if no selection', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: null, + end: null, + }, + editor: { + present: { + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + attributes: { + clientId1: {}, + clientId2: {}, + }, + order: { + '': [ 'clientId1', 'clientId2' ], + clientId1: [], + clientId2: [], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + insertionPoint: null, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 2, + } ); + } ); + } ); + + describe( 'isBlockInsertionPointVisible', () => { + it( 'should return false if no assigned insertion point', () => { + const state = { + insertionPoint: null, + }; + + expect( isBlockInsertionPointVisible( state ) ).toBe( false ); + } ); + + it( 'should return true if assigned insertion point', () => { + const state = { + insertionPoint: { + rootClientId: undefined, + index: 5, + }, + }; + + expect( isBlockInsertionPointVisible( state ) ).toBe( true ); + } ); + } ); + + describe( 'canInsertBlockType', () => { + it( 'should deny blocks that are not registered', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + }, + }, + }, + blockListSettings: {}, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/invalid' ) ).toBe( false ); + } ); + + it( 'should deny blocks that are not allowed by the editor', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + }, + }, + }, + blockListSettings: {}, + settings: { + allowedBlockTypes: [], + }, + }; + expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( false ); + } ); + + it( 'should allow blocks that are allowed by the editor', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + }, + }, + }, + blockListSettings: {}, + settings: { + allowedBlockTypes: [ 'core/test-block-a' ], + }, + }; + expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( true ); + } ); + + it( 'should deny blocks when the editor has a template lock', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + }, + }, + }, + blockListSettings: {}, + settings: { + templateLock: 'all', + }, + }; + expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( false ); + } ); + + it( 'should deny blocks that restrict parent from being inserted into the root', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + }, + }, + }, + blockListSettings: {}, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-c' ) ).toBe( false ); + } ); + + it( 'should deny blocks that restrict parent from being inserted into a restricted parent', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + }, + }, + }, + }, + blockListSettings: {}, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( false ); + } ); + + it( 'should allow blocks that restrict parent to be inserted into an allowed parent', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, + attributes: { + block1: {}, + }, + }, + }, + }, + blockListSettings: {}, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( true ); + } ); + + it( 'should deny restricted blocks from being inserted into a block that restricts allowedBlocks', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + }, + }, + }, + }, + blockListSettings: { + block1: { + allowedBlocks: [ 'core/test-block-c' ], + }, + }, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-b', 'block1' ) ).toBe( false ); + } ); + + it( 'should allow allowed blocks to be inserted into a block that restricts allowedBlocks', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + }, + }, + }, + }, + blockListSettings: { + block1: { + allowedBlocks: [ 'core/test-block-b' ], + }, + }, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-b', 'block1' ) ).toBe( true ); + } ); + + it( 'should prioritise parent over allowedBlocks', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, + attributes: { + block1: {}, + }, + }, + }, + }, + blockListSettings: { + block1: { + allowedBlocks: [], + }, + }, + settings: {}, + }; + expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( true ); + } ); + } ); + + describe( 'getInserterItems', () => { + it( 'should properly list block type and reusable block items', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + }, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + settings: { + __experimentalReusableBlocks: [ + { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, + ], + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + }; + const items = getInserterItems( state ); + const testBlockAItem = items.find( ( item ) => item.id === 'core/test-block-a' ); + expect( testBlockAItem ).toEqual( { + id: 'core/test-block-a', + name: 'core/test-block-a', + initialAttributes: {}, + title: 'Test Block A', + icon: { + src: 'test', + }, + category: 'formatting', + keywords: [ 'testing' ], + isDisabled: false, + utility: 0, + frecency: 0, + hasChildBlocksWithInserterSupport: false, + } ); + const reusableBlockItem = items.find( ( item ) => item.id === 'core/block/1' ); + expect( reusableBlockItem ).toEqual( { + id: 'core/block/1', + name: 'core/block', + initialAttributes: { ref: 1 }, + title: 'Reusable Block 1', + icon: { + src: 'test', + }, + category: 'reusable', + keywords: [], + isDisabled: false, + utility: 0, + frecency: 0, + } ); + } ); + + it( 'should not list a reusable block item if it is being inserted inside it self', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1ref: { + name: 'core/block', + clientId: 'block1ref', + }, + itselfBlock1: { name: 'core/test-block-a' }, + itselfBlock2: { name: 'core/test-block-b' }, + }, + attributes: { + block1ref: { + attributes: { + ref: 1, + }, + }, + itselfBlock1: {}, + itselfBlock2: {}, + }, + order: { + '': [ 'block1ref' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + settings: { + __experimentalReusableBlocks: [ + { id: 1, isTemporary: false, clientId: 'itselfBlock1', title: 'Reusable Block 1' }, + { id: 2, isTemporary: false, clientId: 'itselfBlock2', title: 'Reusable Block 2' }, + ], + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + }; + const items = getInserterItems( state, 'itselfBlock1' ); + const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); + expect( reusableBlockItems ).toHaveLength( 1 ); + expect( reusableBlockItems[ 0 ] ).toEqual( { + id: 'core/block/2', + name: 'core/block', + initialAttributes: { ref: 2 }, + title: 'Reusable Block 2', + icon: { + src: 'test', + }, + category: 'reusable', + keywords: [], + isDisabled: false, + utility: 0, + frecency: 0, + } ); + } ); + + it( 'should not list a reusable block item if it is being inserted inside a descendent', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block2ref: { + name: 'core/block', + clientId: 'block1ref', + }, + referredBlock1: { name: 'core/test-block-a' }, + referredBlock2: { name: 'core/test-block-b' }, + childReferredBlock2: { name: 'core/test-block-a' }, + grandchildReferredBlock2: { name: 'core/test-block-b' }, + }, + attributes: { + block2ref: { + attributes: { + ref: 2, + }, + }, + referredBlock1: {}, + referredBlock2: {}, + childReferredBlock2: {}, + grandchildReferredBlock2: {}, + }, + order: { + '': [ 'block2ref' ], + referredBlock2: [ 'childReferredBlock2' ], + childReferredBlock2: [ 'grandchildReferredBlock2' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + settings: { + __experimentalReusableBlocks: [ + { id: 1, isTemporary: false, clientId: 'referredBlock1', title: 'Reusable Block 1' }, + { id: 2, isTemporary: false, clientId: 'referredBlock2', title: 'Reusable Block 2' }, + ], + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + }; + const items = getInserterItems( state, 'grandchildReferredBlock2' ); + const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); + expect( reusableBlockItems ).toHaveLength( 1 ); + expect( reusableBlockItems[ 0 ] ).toEqual( { + id: 'core/block/1', + name: 'core/block', + initialAttributes: { ref: 1 }, + title: 'Reusable Block 1', + icon: { + src: 'test', + }, + category: 'reusable', + keywords: [], + isDisabled: false, + utility: 0, + frecency: 0, + } ); + } ); + it( 'should order items by descending utility and frecency', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + block2: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + block2: {}, + }, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + settings: { + __experimentalReusableBlocks: [ + { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, + { id: 2, isTemporary: false, clientId: 'block2', title: 'Reusable Block 2' }, + ], + }, + currentPost: {}, + preferences: { + insertUsage: { + 'core/block/1': { count: 10, time: 1000 }, + 'core/block/2': { count: 20, time: 1000 }, + }, + }, + blockListSettings: {}, + }; + const itemIDs = getInserterItems( state ).map( ( item ) => item.id ); + expect( itemIDs ).toEqual( [ + 'core/block/2', + 'core/block/1', + 'core/test-block-b', + 'core/test-freeform', + 'core/test-block-a', + ] ); + } ); + + it( 'should correctly cache the return values', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + block2: { name: 'core/test-block-a' }, + block3: { name: 'core/test-block-a' }, + block4: { name: 'core/test-block-a' }, + }, + attributes: { + block1: {}, + block2: {}, + block3: {}, + block4: {}, + }, + order: { + '': [ 'block3', 'block4' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + settings: { + __experimentalReusableBlocks: [ + { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, + { id: 2, isTemporary: false, clientId: 'block2', title: 'Reusable Block 2' }, + ], + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + }; + + const stateSecondBlockRestricted = { + ...state, + blockListSettings: { + block4: { + allowedBlocks: [ 'core/test-block-b' ], + }, + }, + }; + + const firstBlockFirstCall = getInserterItems( state, 'block3' ); + const firstBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block3' ); + expect( firstBlockFirstCall ).toBe( firstBlockSecondCall ); + expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ + 'core/test-block-b', + 'core/test-freeform', + 'core/test-block-a', + 'core/block/1', + 'core/block/2', + ] ); + + const secondBlockFirstCall = getInserterItems( state, 'block4' ); + const secondBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block4' ); + expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ + 'core/test-block-b', + 'core/test-freeform', + 'core/test-block-a', + 'core/block/1', + 'core/block/2', + ] ); + expect( secondBlockSecondCall.map( ( item ) => item.id ) ).toEqual( [ + 'core/test-block-b', + ] ); + } ); + + it( 'should set isDisabled when a block with `multiple: false` has been used', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { clientId: 'block1', name: 'core/test-block-b' }, + }, + attributes: { + block1: { attribute: {} }, + }, + order: { + '': [ 'block1' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: {}, + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state ); + const testBlockBItem = items.find( ( item ) => item.id === 'core/test-block-b' ); + expect( testBlockBItem.isDisabled ).toBe( true ); + } ); + + it( 'should give common blocks a low utility', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: {}, + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state ); + const testBlockBItem = items.find( ( item ) => item.id === 'core/test-block-b' ); + expect( testBlockBItem.utility ).toBe( INSERTER_UTILITY_LOW ); + } ); + + it( 'should give used blocks a medium utility and set a frecency', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: {}, + }, + currentPost: {}, + preferences: { + insertUsage: { + 'core/test-block-b': { count: 10, time: 1000 }, + }, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state ); + const reusableBlock2Item = items.find( ( item ) => item.id === 'core/test-block-b' ); + expect( reusableBlock2Item.utility ).toBe( INSERTER_UTILITY_MEDIUM ); + expect( reusableBlock2Item.frecency ).toBe( 2.5 ); + } ); + + it( 'should give contextual blocks a high utility', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, + attributes: { + block1: { attribute: {} }, + }, + order: { + '': [ 'block1' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: {}, + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state, 'block1' ); + const testBlockCItem = items.find( ( item ) => item.id === 'core/test-block-c' ); + expect( testBlockCItem.utility ).toBe( INSERTER_UTILITY_HIGH ); + } ); + } ); + + describe( 'isValidTemplate', () => { + it( 'should return true if template is valid', () => { + const state = { + template: { isValid: true }, + }; + + expect( isValidTemplate( state ) ).toBe( true ); + } ); + + it( 'should return false if template is not valid', () => { + const state = { + template: { isValid: false }, + }; + + expect( isValidTemplate( state ) ).toBe( false ); + } ); + } ); + + describe( 'getTemplate', () => { + it( 'should return the template object', () => { + const template = []; + const state = { + settings: { template }, + }; + + expect( getTemplate( state ) ).toBe( template ); + } ); + } ); + + describe( 'getTemplateLock', () => { + it( 'should return the general template lock if no clientId was set', () => { + const state = { + settings: { templateLock: 'all' }, + }; + + expect( getTemplateLock( state ) ).toBe( 'all' ); + } ); + + it( 'should return null if the specified clientId was not found ', () => { + const state = { + settings: { templateLock: 'all' }, + blockListSettings: { + chicken: { + templateLock: 'insert', + }, + }, + }; + + expect( getTemplateLock( state, 'ribs' ) ).toBe( null ); + } ); + + it( 'should return null if template lock was not set on the specified block', () => { + const state = { + settings: { templateLock: 'all' }, + blockListSettings: { + chicken: { + test: 'tes1t', + }, + }, + }; + + expect( getTemplateLock( state, 'ribs' ) ).toBe( null ); + } ); + + it( 'should return the template lock for the specified clientId', () => { + const state = { + settings: { templateLock: 'all' }, + blockListSettings: { + chicken: { + templateLock: 'insert', + }, + }, + }; + + expect( getTemplateLock( state, 'chicken' ) ).toBe( 'insert' ); + } ); + } ); + + describe( 'getBlockListSettings', () => { + it( 'should return the settings of a block', () => { + const state = { + blockListSettings: { + chicken: { + setting1: false, + }, + ribs: { + setting2: true, + }, + }, + }; + + expect( getBlockListSettings( state, 'chicken' ) ).toEqual( { + setting1: false, + } ); + } ); + + it( 'should return undefined if settings for the block don’t exist', () => { + const state = { + blockListSettings: {}, + }; + + expect( getBlockListSettings( state, 'chicken' ) ).toBe( undefined ); + } ); + } ); +} ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index b4dacc0f41d5d..47d1c4d03c6fd 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -3,6 +3,11 @@ */ import { castArray } from 'lodash'; +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + /** * Returns an action object used in signalling that editor has initialized with * the specified post object and editor settings. @@ -326,7 +331,7 @@ export function updateEditorBlocks( blocks ) { */ const getBlockEditorAction = ( name ) => ( ...args ) => { - window.wp.data.dispatch( 'core/block-editor' )[ name ]( ...args ); + dispatch( 'core/block-editor' )[ name ]( ...args ); return { type: 'DO_NOTHING' }; }; diff --git a/packages/editor/src/store/array.js b/packages/editor/src/store/array.js deleted file mode 100644 index 176d8936450af..0000000000000 --- a/packages/editor/src/store/array.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import { castArray } from 'lodash'; - -/** - * Insert one or multiple elements into a given position of an array. - * - * @param {Array} array Source array. - * @param {*} elements Elements to insert. - * @param {number} index Insert Position. - * - * @return {Array} Result. - */ -export function insertAt( array, elements, index ) { - return [ - ...array.slice( 0, index ), - ...castArray( elements ), - ...array.slice( index ), - ]; -} - -/** - * Moves an element in an array. - * - * @param {Array} array Source array. - * @param {number} from Source index. - * @param {number} to Destination index. - * @param {number} count Number of elements to move. - * - * @return {Array} Result. - */ -export function moveTo( array, from, to, count = 1 ) { - const withoutMovedElements = [ ...array ]; - withoutMovedElements.splice( from, count ); - return insertAt( - withoutMovedElements, - array.slice( from, from + count ), - to, - ); -} diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 071e4f07f4f6f..8e2d2b263e2fa 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, reduce } from 'lodash'; +import { noop } from 'lodash'; /** * WordPress dependencies @@ -12,6 +12,7 @@ import { unregisterBlockType, createBlock, } from '@wordpress/blocks'; +import { dispatch as dataDispatch, select as dataSelect } from '@wordpress/data'; /** * Internal dependencies @@ -25,11 +26,8 @@ import { convertBlockToReusable, } from '../reusable-blocks'; import { - resetBlocks, - receiveBlocks, __experimentalSaveReusableBlock as saveReusableBlock, __experimentalDeleteReusableBlock as deleteReusableBlock, - removeBlocks, __experimentalConvertBlockToReusable as convertBlockToReusableAction, __experimentalConvertBlockToStatic as convertBlockToStaticAction, __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, @@ -50,6 +48,7 @@ describe( 'reusable blocks effects', () => { name: { type: 'string' }, }, } ); + registerBlockType( 'core/block', { title: 'Reusable Block', category: 'common', @@ -58,11 +57,15 @@ describe( 'reusable blocks effects', () => { ref: { type: 'string' }, }, } ); + + // jest.spyOn( dataDispatch( 'core/block-editor' ), 'createErrorNotice' ); } ); afterAll( () => { unregisterBlockType( 'core/test-block' ); unregisterBlockType( 'core/block' ); + + // dataDispatch( 'core/block-editor' ).createErrorNotice.mockReset(); } ); describe( 'fetchReusableBlocks', () => { @@ -252,10 +255,8 @@ describe( 'reusable blocks effects', () => { const reusableBlock = { id: 123, title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -267,6 +268,8 @@ describe( 'reusable blocks effects', () => { id: 123, updatedId: 456, } ); + + dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); it( 'should handle an API error', async () => { @@ -286,10 +289,8 @@ describe( 'reusable blocks effects', () => { const reusableBlock = { id: 123, title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -299,6 +300,8 @@ describe( 'reusable blocks effects', () => { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id: 123, } ); + + dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); } ); @@ -310,9 +313,13 @@ describe( 'reusable blocks effects', () => { }, ] ); - expect( receiveReusableBlocks( action ) ).toEqual( receiveBlocks( [ + jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); + receiveReusableBlocks( action ); + expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( [ { clientId: 'broccoli' }, - ] ) ); + ] ); + + dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); } ); } ); @@ -335,11 +342,12 @@ describe( 'reusable blocks effects', () => { const reusableBlock = { id: 123, title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ + associatedBlock, + parsedBlock, + ] ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -352,8 +360,8 @@ describe( 'reusable blocks effects', () => { optimist: expect.any( Object ), } ); - expect( dispatch ).toHaveBeenCalledWith( - removeBlocks( [ associatedBlock.clientId, parsedBlock.clientId ] ) + expect( dataDispatch( 'core/block-editor' ).removeBlocks ).toHaveBeenCalledWith( + [ associatedBlock.clientId, parsedBlock.clientId ] ); expect( dispatch ).toHaveBeenCalledWith( { @@ -361,6 +369,9 @@ describe( 'reusable blocks effects', () => { id: 123, optimist: expect.any( Object ), } ); + + dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlocks.mockReset(); } ); it( 'should handle an API error', async () => { @@ -379,11 +390,11 @@ describe( 'reusable blocks effects', () => { const reusableBlock = { id: 123, title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ + parsedBlock, + ] ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -395,16 +406,19 @@ describe( 'reusable blocks effects', () => { id: 123, optimist: expect.any( Object ), } ); + dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlocks.mockReset(); } ); it( 'should not save reusable blocks with temporary IDs', async () => { const reusableBlock = { id: 'reusable1', title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ + parsedBlock, + ] ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -412,6 +426,8 @@ describe( 'reusable blocks effects', () => { await deleteReusableBlocks( deleteReusableBlock( 'reusable1' ), store ); expect( dispatch ).not.toHaveBeenCalled(); + dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlocks.mockReset(); } ); } ); @@ -421,28 +437,29 @@ describe( 'reusable blocks effects', () => { const reusableBlock = { id: 123, title: 'My cool block' }; const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => + associatedBlock.clientId === id ? associatedBlock : parsedBlock + ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - clientIds: [ associatedBlock.clientId ], - blocks: [ + expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( + associatedBlock.clientId, + [ expect.objectContaining( { name: 'core/test-block', attributes: { name: 'Big Bird' }, } ), - ], - time: expect.any( Number ), - } ); + ] + ); + + dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); it( 'should convert a reusable block with nested blocks into a static block', () => { @@ -452,22 +469,20 @@ describe( 'reusable blocks effects', () => { createBlock( 'core/test-block', { name: 'Oscar the Grouch' } ), createBlock( 'core/test-block', { name: 'Cookie Monster' } ), ] ); - - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); + const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => + associatedBlock.clientId === id ? associatedBlock : parsedBlock + ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - clientIds: [ associatedBlock.clientId ], - blocks: [ + expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( + associatedBlock.clientId, + [ expect.objectContaining( { name: 'core/test-block', attributes: { name: 'Big Bird' }, @@ -480,19 +495,25 @@ describe( 'reusable blocks effects', () => { } ), ], } ), - ], - time: expect.any( Number ), - } ); + ] + ); + + dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); } ); describe( 'convertBlockToReusable', () => { it( 'should convert a static block into a reusable block', () => { const staticBlock = createBlock( 'core/block', { ref: 123 } ); - const state = reducer( undefined, resetBlocks( [ staticBlock ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( ) => + staticBlock + ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); + jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + const store = { getState: () => {}, dispatch }; convertBlockToReusable( convertBlockToReusableAction( staticBlock.clientId ), store ); @@ -511,21 +532,21 @@ describe( 'reusable blocks effects', () => { saveReusableBlock( expect.stringMatching( /^reusable/ ) ), ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - clientIds: [ staticBlock.clientId ], - blocks: [ - expect.objectContaining( { - name: 'core/block', - attributes: { ref: expect.stringMatching( /^reusable/ ) }, - } ), - ], - time: expect.any( Number ), - } ); + expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( + [ staticBlock.clientId ], + expect.objectContaining( { + name: 'core/block', + attributes: { ref: expect.stringMatching( /^reusable/ ) }, + } ), + ); - expect( dispatch ).toHaveBeenCalledWith( - receiveBlocks( [ staticBlock ] ), + expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( + [ staticBlock ] ); + + dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); + dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); + dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); } ); } ); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 384b824e7ecd8..8de29078fd312 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -7,9 +7,7 @@ import { reduce, omit, mapValues, - keys, isEqual, - overSome, get, } from 'lodash'; @@ -62,75 +60,6 @@ function getMutateSafeObject( original, working ) { return working; } -/** - * Returns true if the two object arguments have the same keys, or false - * otherwise. - * - * @param {Object} a First object. - * @param {Object} b Second object. - * - * @return {boolean} Whether the two objects have the same keys. - */ -export function hasSameKeys( a, b ) { - return isEqual( keys( a ), keys( b ) ); -} - -/** - * Returns true if, given the currently dispatching action and the previously - * dispatched action, the two actions are updating the same block attribute, or - * false otherwise. - * - * @param {Object} action Currently dispatching action. - * @param {Object} previousAction Previously dispatched action. - * - * @return {boolean} Whether actions are updating the same block attribute. - */ -export function isUpdatingSameBlockAttribute( action, previousAction ) { - return ( - action.type === 'UPDATE_BLOCK_ATTRIBUTES' && - action.clientId === previousAction.clientId && - hasSameKeys( action.attributes, previousAction.attributes ) - ); -} - -/** - * Returns true if, given the currently dispatching action and the previously - * dispatched action, the two actions are editing the same post property, or - * false otherwise. - * - * @param {Object} action Currently dispatching action. - * @param {Object} previousAction Previously dispatched action. - * - * @return {boolean} Whether actions are updating the same post property. - */ -export function isUpdatingSamePostProperty( action, previousAction ) { - return ( - action.type === 'EDIT_POST' && - hasSameKeys( action.edits, previousAction.edits ) - ); -} - -/** - * Returns true if, given the currently dispatching action and the previously - * dispatched action, the two actions are modifying the same property such that - * undo history should be batched. - * - * @param {Object} action Currently dispatching action. - * @param {Object} previousAction Previously dispatched action. - * - * @return {boolean} Whether to overwrite present state. - */ -export function shouldOverwriteState( action, previousAction ) { - if ( ! previousAction || action.type !== previousAction.type ) { - return false; - } - - return overSome( [ - isUpdatingSameBlockAttribute, - isUpdatingSamePostProperty, - ] )( action, previousAction ); -} - /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 34195de66ed38..39b5c5f5bdec3 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -21,6 +21,7 @@ import { import { isInTheFuture, getDate } from '@wordpress/date'; import { removep } from '@wordpress/autop'; import { addQueryArgs } from '@wordpress/url'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -1063,7 +1064,7 @@ export function isEditorReady( state ) { */ const getBlockEditorSelector = ( name ) => ( state, ...args ) => { - return window.wp.data.select( 'core/block-editor' )[ name ]( ...args ); + return select( 'core/block-editor' )[ name ]( ...args ); }; export const getBlockDependantsCacheBust = getBlockEditorSelector( 'getBlockDependantsCacheBust' ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index b071dc78204fb..4875576181b10 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -2,42 +2,16 @@ * Internal dependencies */ import { - replaceBlocks, - startTyping, - stopTyping, - enterFormattedText, - exitFormattedText, __experimentalFetchReusableBlocks as fetchReusableBlocks, __experimentalSaveReusableBlock as saveReusableBlock, __experimentalDeleteReusableBlock as deleteReusableBlock, __experimentalConvertBlockToStatic as convertBlockToStatic, __experimentalConvertBlockToReusable as convertBlockToReusable, - toggleSelection, setupEditor, resetPost, - resetBlocks, - updateBlockAttributes, - updateBlock, - selectBlock, - startMultiSelect, - stopMultiSelect, - multiSelect, - clearSelectedBlock, - replaceBlock, - insertBlock, - insertBlocks, - showInsertionPoint, - hideInsertionPoint, editPost, savePost, trashPost, - mergeBlocks, - redo, - undo, - removeBlocks, - removeBlock, - toggleBlockMode, - updateBlockListSettings, } from '../actions'; describe( 'actions', () => { @@ -62,169 +36,6 @@ describe( 'actions', () => { } ); } ); } ); - describe( 'resetBlocks', () => { - it( 'should return the RESET_BLOCKS actions', () => { - const blocks = []; - const result = resetBlocks( blocks ); - expect( result ).toEqual( { - type: 'RESET_BLOCKS', - blocks, - } ); - } ); - } ); - - describe( 'updateBlockAttributes', () => { - it( 'should return the UPDATE_BLOCK_ATTRIBUTES action', () => { - const clientId = 'myclientid'; - const attributes = {}; - const result = updateBlockAttributes( clientId, attributes ); - expect( result ).toEqual( { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId, - attributes, - } ); - } ); - } ); - - describe( 'updateBlock', () => { - it( 'should return the UPDATE_BLOCK action', () => { - const clientId = 'myclientid'; - const updates = {}; - const result = updateBlock( clientId, updates ); - expect( result ).toEqual( { - type: 'UPDATE_BLOCK', - clientId, - updates, - } ); - } ); - } ); - - describe( 'selectBlock', () => { - it( 'should return the SELECT_BLOCK action', () => { - const clientId = 'myclientid'; - const result = selectBlock( clientId, -1 ); - expect( result ).toEqual( { - type: 'SELECT_BLOCK', - initialPosition: -1, - clientId, - } ); - } ); - } ); - - describe( 'startMultiSelect', () => { - it( 'should return the START_MULTI_SELECT', () => { - expect( startMultiSelect() ).toEqual( { - type: 'START_MULTI_SELECT', - } ); - } ); - } ); - - describe( 'stopMultiSelect', () => { - it( 'should return the Stop_MULTI_SELECT', () => { - expect( stopMultiSelect() ).toEqual( { - type: 'STOP_MULTI_SELECT', - } ); - } ); - } ); - describe( 'multiSelect', () => { - it( 'should return MULTI_SELECT action', () => { - const start = 'start'; - const end = 'end'; - expect( multiSelect( start, end ) ).toEqual( { - type: 'MULTI_SELECT', - start, - end, - } ); - } ); - } ); - - describe( 'clearSelectedBlock', () => { - it( 'should return CLEAR_SELECTED_BLOCK action', () => { - expect( clearSelectedBlock() ).toEqual( { - type: 'CLEAR_SELECTED_BLOCK', - } ); - } ); - } ); - - describe( 'replaceBlock', () => { - it( 'should return the REPLACE_BLOCKS action', () => { - const block = { - clientId: 'ribs', - }; - - expect( replaceBlock( [ 'chicken' ], block ) ).toEqual( { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [ block ], - time: expect.any( Number ), - } ); - } ); - } ); - - describe( 'replaceBlocks', () => { - it( 'should return the REPLACE_BLOCKS action', () => { - const blocks = [ { - clientId: 'ribs', - } ]; - - expect( replaceBlocks( [ 'chicken' ], blocks ) ).toEqual( { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks, - time: expect.any( Number ), - } ); - } ); - } ); - - describe( 'insertBlock', () => { - it( 'should return the INSERT_BLOCKS action', () => { - const block = { - clientId: 'ribs', - }; - const index = 5; - expect( insertBlock( block, index, 'testclientid' ) ).toEqual( { - type: 'INSERT_BLOCKS', - blocks: [ block ], - index, - rootClientId: 'testclientid', - time: expect.any( Number ), - updateSelection: true, - } ); - } ); - } ); - - describe( 'insertBlocks', () => { - it( 'should return the INSERT_BLOCKS action', () => { - const blocks = [ { - clientId: 'ribs', - } ]; - const index = 3; - expect( insertBlocks( blocks, index, 'testclientid' ) ).toEqual( { - type: 'INSERT_BLOCKS', - blocks, - index, - rootClientId: 'testclientid', - time: expect.any( Number ), - updateSelection: true, - } ); - } ); - } ); - - describe( 'showInsertionPoint', () => { - it( 'should return the SHOW_INSERTION_POINT action', () => { - expect( showInsertionPoint() ).toEqual( { - type: 'SHOW_INSERTION_POINT', - } ); - } ); - } ); - - describe( 'hideInsertionPoint', () => { - it( 'should return the HIDE_INSERTION_POINT action', () => { - expect( hideInsertionPoint() ).toEqual( { - type: 'HIDE_INSERTION_POINT', - } ); - } ); - } ); describe( 'editPost', () => { it( 'should return EDIT_POST action', () => { @@ -264,106 +75,6 @@ describe( 'actions', () => { } ); } ); - describe( 'mergeBlocks', () => { - it( 'should return MERGE_BLOCKS action', () => { - const firstBlockClientId = 'blockA'; - const secondBlockClientId = 'blockB'; - expect( mergeBlocks( firstBlockClientId, secondBlockClientId ) ).toEqual( { - type: 'MERGE_BLOCKS', - blocks: [ firstBlockClientId, secondBlockClientId ], - } ); - } ); - } ); - - describe( 'redo', () => { - it( 'should return REDO action', () => { - expect( redo() ).toEqual( { - type: 'REDO', - } ); - } ); - } ); - - describe( 'undo', () => { - it( 'should return UNDO action', () => { - expect( undo() ).toEqual( { - type: 'UNDO', - } ); - } ); - } ); - - describe( 'removeBlocks', () => { - it( 'should return REMOVE_BLOCKS action', () => { - const clientIds = [ 'clientId' ]; - expect( removeBlocks( clientIds ) ).toEqual( { - type: 'REMOVE_BLOCKS', - clientIds, - selectPrevious: true, - } ); - } ); - } ); - - describe( 'removeBlock', () => { - it( 'should return REMOVE_BLOCKS action', () => { - const clientId = 'myclientid'; - expect( removeBlock( clientId ) ).toEqual( { - type: 'REMOVE_BLOCKS', - clientIds: [ - clientId, - ], - selectPrevious: true, - } ); - expect( removeBlock( clientId, false ) ).toEqual( { - type: 'REMOVE_BLOCKS', - clientIds: [ - clientId, - ], - selectPrevious: false, - } ); - } ); - } ); - - describe( 'toggleBlockMode', () => { - it( 'should return TOGGLE_BLOCK_MODE action', () => { - const clientId = 'myclientid'; - expect( toggleBlockMode( clientId ) ).toEqual( { - type: 'TOGGLE_BLOCK_MODE', - clientId, - } ); - } ); - } ); - - describe( 'startTyping', () => { - it( 'should return the START_TYPING action', () => { - expect( startTyping() ).toEqual( { - type: 'START_TYPING', - } ); - } ); - } ); - - describe( 'stopTyping', () => { - it( 'should return the STOP_TYPING action', () => { - expect( stopTyping() ).toEqual( { - type: 'STOP_TYPING', - } ); - } ); - } ); - - describe( 'enterFormattedText', () => { - it( 'should return the ENTER_FORMATTED_TEXT action', () => { - expect( enterFormattedText() ).toEqual( { - type: 'ENTER_FORMATTED_TEXT', - } ); - } ); - } ); - - describe( 'exitFormattedText', () => { - it( 'should return the EXIT_FORMATTED_TEXT action', () => { - expect( exitFormattedText() ).toEqual( { - type: 'EXIT_FORMATTED_TEXT', - } ); - } ); - } ); - describe( 'fetchReusableBlocks', () => { it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { expect( fetchReusableBlocks() ).toEqual( { @@ -416,45 +127,4 @@ describe( 'actions', () => { } ); } ); } ); - - describe( 'toggleSelection', () => { - it( 'should return the TOGGLE_SELECTION action with default value for isSelectionEnabled = true', () => { - expect( toggleSelection() ).toEqual( { - type: 'TOGGLE_SELECTION', - isSelectionEnabled: true, - } ); - } ); - - it( 'should return the TOGGLE_SELECTION action with isSelectionEnabled = true as passed in the argument', () => { - expect( toggleSelection( true ) ).toEqual( { - type: 'TOGGLE_SELECTION', - isSelectionEnabled: true, - } ); - } ); - - it( 'should return the TOGGLE_SELECTION action with isSelectionEnabled = false as passed in the argument', () => { - expect( toggleSelection( false ) ).toEqual( { - type: 'TOGGLE_SELECTION', - isSelectionEnabled: false, - } ); - } ); - } ); - - describe( 'updateBlockListSettings', () => { - it( 'should return the UPDATE_BLOCK_LIST_SETTINGS with undefined settings', () => { - expect( updateBlockListSettings( 'chicken' ) ).toEqual( { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: 'chicken', - settings: undefined, - } ); - } ); - - it( 'should return the UPDATE_BLOCK_LIST_SETTINGS action with the passed settings', () => { - expect( updateBlockListSettings( 'chicken', { chicken: 'ribs' } ) ).toEqual( { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: 'chicken', - settings: { chicken: 'ribs' }, - } ); - } ); - } ); } ); diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index a6ce470d86e54..8ee0cde5242e4 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - /** * WordPress dependencies */ @@ -10,27 +5,15 @@ import { getBlockTypes, unregisterBlockType, registerBlockType, - createBlock, } from '@wordpress/blocks'; -import { dispatch as dataDispatch, createRegistry } from '@wordpress/data'; +import { dispatch as dataDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import actions, { - updateEditorSettings, - setupEditorState, - mergeBlocks, - replaceBlocks, - resetBlocks, - selectBlock, - setTemplateValidity, -} from '../actions'; -import effects, { validateBlocksToTemplate } from '../effects'; +import { setupEditorState, updateEditorBlocks } from '../actions'; +import effects from '../effects'; import { SAVE_POST_NOTICE_ID } from '../effects/posts'; -import * as selectors from '../selectors'; -import reducer from '../reducer'; -import applyMiddlewares from '../middlewares'; import '../../'; describe( 'effects', () => { @@ -46,178 +29,6 @@ describe( 'effects', () => { const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; - describe( '.MERGE_BLOCKS', () => { - const handler = effects.MERGE_BLOCKS; - const defaultGetBlock = selectors.getBlock; - - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); - selectors.getBlock = defaultGetBlock; - } ); - - it( 'should only focus the blockA if the blockA has no merge function', () => { - registerBlockType( 'core/test-block', defaultBlockSettings ); - const blockA = { - clientId: 'chicken', - name: 'core/test-block', - }; - const blockB = { - clientId: 'ribs', - name: 'core/test-block', - }; - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); - - expect( dispatch ).toHaveBeenCalledTimes( 1 ); - expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken' ) ); - } ); - - it( 'should merge the blocks if blocks of the same type', () => { - registerBlockType( 'core/test-block', { - merge( attributes, attributesToMerge ) { - return { - content: attributes.content + ' ' + attributesToMerge.content, - }; - }, - save: noop, - category: 'common', - title: 'test block', - } ); - const blockA = { - clientId: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken' }, - }; - const blockB = { - clientId: 'ribs', - name: 'core/test-block', - attributes: { content: 'ribs' }, - }; - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); - - expect( dispatch ).toHaveBeenCalledTimes( 2 ); - expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken', -1 ) ); - expect( dispatch ).toHaveBeenCalledWith( { - ...replaceBlocks( [ 'chicken', 'ribs' ], [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken ribs' }, - } ] ), - time: expect.any( Number ), - } ); - } ); - - it( 'should not merge the blocks have different types without transformation', () => { - registerBlockType( 'core/test-block', { - merge( attributes, attributesToMerge ) { - return { - content: attributes.content + ' ' + attributesToMerge.content, - }; - }, - save: noop, - category: 'common', - title: 'test block', - } ); - registerBlockType( 'core/test-block-2', defaultBlockSettings ); - const blockA = { - clientId: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken' }, - }; - const blockB = { - clientId: 'ribs', - name: 'core/test-block2', - attributes: { content: 'ribs' }, - }; - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); - - expect( dispatch ).not.toHaveBeenCalled(); - } ); - - it( 'should transform and merge the blocks', () => { - registerBlockType( 'core/test-block', { - attributes: { - content: { - type: 'string', - }, - }, - merge( attributes, attributesToMerge ) { - return { - content: attributes.content + ' ' + attributesToMerge.content, - }; - }, - save: noop, - category: 'common', - title: 'test block', - } ); - registerBlockType( 'core/test-block-2', { - attributes: { - content: { - type: 'string', - }, - }, - transforms: { - to: [ { - type: 'block', - blocks: [ 'core/test-block' ], - transform: ( { content2 } ) => { - return createBlock( 'core/test-block', { - content: content2, - } ); - }, - } ], - }, - save: noop, - category: 'common', - title: 'test block 2', - } ); - const blockA = { - clientId: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken' }, - }; - const blockB = { - clientId: 'ribs', - name: 'core/test-block-2', - attributes: { content2: 'ribs' }, - }; - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { dispatch, getState } ); - - expect( dispatch ).toHaveBeenCalledTimes( 2 ); - // expect( dispatch ).toHaveBeenCalledWith( focusBlock( 'chicken', { offset: -1 } ) ); - expect( dispatch ).toHaveBeenCalledWith( { - ...replaceBlocks( [ 'chicken', 'ribs' ], [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken ribs' }, - } ] ), - time: expect.any( Number ), - } ); - } ); - } ); - describe( '.REQUEST_POST_UPDATE_SUCCESS', () => { const handler = effects.REQUEST_POST_UPDATE_SUCCESS; @@ -423,19 +234,11 @@ describe( 'effects', () => { }, status: 'draft', }; - const getState = () => ( { - settings: { - template: null, - templateLock: false, - }, - template: { - isValid: true, - }, - } ); - const result = handler( { post, settings: {} }, { getState } ); + const result = handler( { post, settings: {} } ); expect( result ).toEqual( [ + updateEditorBlocks( [] ), setupEditorState( post, [], {} ), ] ); } ); @@ -452,22 +255,11 @@ describe( 'effects', () => { }, status: 'draft', }; - const getState = () => ( { - settings: { - template: null, - templateLock: false, - }, - template: { - isValid: true, - }, - } ); - const result = handler( { post }, { getState } ); + const result = handler( { post } ); expect( result[ 0 ].blocks ).toHaveLength( 1 ); - expect( result ).toEqual( [ - setupEditorState( post, result[ 0 ].blocks, {} ), - ] ); + expect( result[ 1 ] ).toEqual( setupEditorState( post, result[ 0 ].blocks, {} ) ); } ); it( 'should return post setup action only if auto-draft', () => { @@ -481,93 +273,13 @@ describe( 'effects', () => { }, status: 'auto-draft', }; - const getState = () => ( { - settings: { - template: null, - templateLock: false, - }, - template: { - isValid: true, - }, - } ); - const result = handler( { post }, { getState } ); + const result = handler( { post } ); expect( result ).toEqual( [ + updateEditorBlocks( [] ), setupEditorState( post, [], { title: 'A History of Pork' } ), ] ); } ); } ); - - describe( 'validateBlocksToTemplate', () => { - let store; - beforeEach( () => { - store = createRegistry().registerStore( 'test', { - actions, - selectors, - reducer, - } ); - applyMiddlewares( store ); - - registerBlockType( 'core/test-block', defaultBlockSettings ); - } ); - - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); - } ); - - it( 'should return undefined if no template assigned', () => { - const result = validateBlocksToTemplate( resetBlocks( [ - createBlock( 'core/test-block' ), - ] ), store ); - - expect( result ).toBe( undefined ); - } ); - - it( 'should return undefined if invalid but unlocked', () => { - store.dispatch( updateEditorSettings( { - template: [ - [ 'core/foo', {} ], - ], - } ) ); - - const result = validateBlocksToTemplate( resetBlocks( [ - createBlock( 'core/test-block' ), - ] ), store ); - - expect( result ).toBe( undefined ); - } ); - - it( 'should return undefined if locked and valid', () => { - store.dispatch( updateEditorSettings( { - template: [ - [ 'core/test-block' ], - ], - templateLock: 'all', - } ) ); - - const result = validateBlocksToTemplate( resetBlocks( [ - createBlock( 'core/test-block' ), - ] ), store ); - - expect( result ).toBe( undefined ); - } ); - - it( 'should return validity set action if invalid on default state', () => { - store.dispatch( updateEditorSettings( { - template: [ - [ 'core/foo' ], - ], - templateLock: 'all', - } ) ); - - const result = validateBlocksToTemplate( resetBlocks( [ - createBlock( 'core/test-block' ), - ] ), store ); - - expect( result ).toEqual( setTemplateValidity( false ) ); - } ); - } ); } ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 2d11a0e319a2d..5f84a7af97497 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1,986 +1,41 @@ /** * External dependencies */ -import { values, noop } from 'lodash'; import deepFreeze from 'deep-freeze'; -/** - * WordPress dependencies - */ -import { - registerBlockType, - unregisterBlockType, - createBlock, -} from '@wordpress/blocks'; - /** * Internal dependencies */ import { - hasSameKeys, - isUpdatingSameBlockAttribute, - isUpdatingSamePostProperty, - shouldOverwriteState, getPostRawValue, - editor, initialEdits, + editor, currentPost, - isTyping, - isCaretWithinFormattedText, - blockSelection, preferences, saving, - blocksMode, - insertionPoint, reusableBlocks, - template, - blockListSettings, autosave, - postSavingLock, - previewLink, -} from '../reducer'; -import { INITIAL_EDITS_DEFAULTS } from '../defaults'; - -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: 'EDIT_POST', - edits: {}, - }; - const previousAction = { - type: 'EDIT_POST', - edits: {}, - }; - - expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return false if not updating the same block', () => { - const action = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 10, - }, - }; - const previousAction = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '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', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 10, - }, - }; - const previousAction = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - bar: 20, - }, - }; - - expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return true if updating the same block attributes', () => { - const action = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 10, - }, - }; - const previousAction = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 20, - }, - }; - - expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( true ); - } ); - } ); - - describe( 'isUpdatingSamePostProperty()', () => { - it( 'should return false if not editing post', () => { - const action = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - attributes: { - foo: 10, - }, - }; - const previousAction = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - attributes: { - foo: 10, - }, - }; - - expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return false if not editing the same post properties', () => { - const action = { - type: 'EDIT_POST', - edits: { - foo: 10, - }, - }; - const previousAction = { - type: 'EDIT_POST', - edits: { - bar: 20, - }, - }; - - expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return true if updating the same post properties', () => { - const action = { - type: 'EDIT_POST', - edits: { - foo: 10, - }, - }; - const previousAction = { - type: 'EDIT_POST', - edits: { - foo: 20, - }, - }; - - expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( true ); - } ); - } ); - - describe( 'shouldOverwriteState()', () => { - it( 'should return false if no previous action', () => { - const action = { - type: 'EDIT_POST', - edits: { - foo: 10, - }, - }; - const previousAction = undefined; - - expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return false if the action types are different', () => { - const action = { - type: 'EDIT_POST', - edits: { - foo: 10, - }, - }; - const previousAction = { - type: 'EDIT_DIFFERENT_POST', - edits: { - foo: 20, - }, - }; - - expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); - } ); - - it( 'should return true if updating same block attribute', () => { - const action = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 10, - }, - }; - const previousAction = { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - attributes: { - foo: 20, - }, - }; - - expect( shouldOverwriteState( action, previousAction ) ).toBe( true ); - } ); - - it( 'should return true if updating same post property', () => { - const action = { - type: 'EDIT_POST', - edits: { - foo: 10, - }, - }; - const previousAction = { - type: 'EDIT_POST', - edits: { - foo: 20, - }, - }; - - expect( shouldOverwriteState( action, previousAction ) ).toBe( true ); - } ); - } ); - - describe( 'getPostRawValue', () => { - it( 'returns original value for non-rendered content', () => { - const value = getPostRawValue( '' ); - - expect( value ).toBe( '' ); - } ); - - it( 'returns raw value for rendered content', () => { - const value = getPostRawValue( { raw: '' } ); - - expect( value ).toBe( '' ); - } ); - } ); - - describe( 'editor()', () => { - beforeAll( () => { - registerBlockType( 'core/test-block', { - save: noop, - edit: noop, - category: 'common', - title: 'test block', - } ); - } ); - - afterAll( () => { - unregisterBlockType( 'core/test-block' ); - } ); - - it( 'should return history (empty edits, blocks) by default', () => { - const state = editor( undefined, {} ); - - expect( state.past ).toEqual( [] ); - expect( state.future ).toEqual( [] ); - expect( state.present.edits ).toEqual( {} ); - expect( state.present.blocks.byClientId ).toEqual( {} ); - expect( state.present.blocks.order ).toEqual( {} ); - expect( state.present.blocks.isDirty ).toBe( false ); - } ); - - it( 'should key by reset blocks clientId', () => { - const original = editor( undefined, {} ); - const state = editor( original, { - type: 'RESET_BLOCKS', - blocks: [ { clientId: 'bananas', innerBlocks: [] } ], - } ); - - expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); - expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'bananas' ); - expect( state.present.blocks.order ).toEqual( { - '': [ 'bananas' ], - bananas: [], - } ); - } ); - - it( 'should key by reset blocks clientId, including inner blocks', () => { - const original = editor( undefined, {} ); - const state = editor( original, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'bananas', - innerBlocks: [ { clientId: 'apples', innerBlocks: [] } ], - } ], - } ); - - expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); - expect( state.present.blocks.order ).toEqual( { - '': [ 'bananas' ], - apples: [], - bananas: [ 'apples' ], - } ); - } ); - - it( 'should insert block', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'INSERT_BLOCKS', - blocks: [ { - clientId: 'ribs', - name: 'core/freeform', - innerBlocks: [], - } ], - } ); - - expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); - expect( values( state.present.blocks.byClientId )[ 1 ].clientId ).toBe( 'ribs' ); - expect( state.present.blocks.order ).toEqual( { - '': [ 'chicken', 'ribs' ], - chicken: [], - ribs: [], - } ); - } ); - - it( 'should replace the block', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [ { - clientId: 'wings', - name: 'core/freeform', - innerBlocks: [], - } ], - } ); - - expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); - expect( values( state.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'wings' ); - expect( state.present.blocks.order ).toEqual( { - '': [ 'wings' ], - wings: [], - } ); - } ); - - 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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - - const state = editor( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ nestedBlock.clientId ], - blocks: [ replacementBlock ], - } ); - - expect( state.present.blocks.order ).toEqual( { - '': [ wrapperBlock.clientId ], - [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], - [ replacementBlock.clientId ]: [], - } ); - } ); - - it( 'should replace the block even if the new block clientId is the same', () => { - const originalState = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const replacedState = editor( originalState, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [ { - clientId: 'chicken', - name: 'core/freeform', - innerBlocks: [], - } ], - } ); - - expect( Object.keys( replacedState.present.blocks.byClientId ) ).toHaveLength( 1 ); - expect( values( originalState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/test-block' ); - expect( values( replacedState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( replacedState.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'chicken' ); - expect( replacedState.present.blocks.order ).toEqual( { - '': [ 'chicken' ], - 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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - - const replacedNestedState = editor( originalNestedState, { - type: 'REPLACE_BLOCKS', - clientIds: [ nestedBlock.clientId ], - blocks: [ replacementNestedBlock ], - } ); - - expect( replacedNestedState.present.blocks.order ).toEqual( { - '': [ wrapperBlock.clientId ], - [ wrapperBlock.clientId ]: [ replacementNestedBlock.clientId ], - [ replacementNestedBlock.clientId ]: [], - } ); - - expect( originalNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/test-block' ); - expect( replacedNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/freeform' ); - } ); - - it( 'should update the block', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - isValid: false, - innerBlocks: [], - } ], - } ); - const state = editor( deepFreeze( original ), { - type: 'UPDATE_BLOCK', - clientId: 'chicken', - updates: { - attributes: { content: 'ribs' }, - isValid: true, - }, - } ); - - expect( state.present.blocks.byClientId.chicken ).toEqual( { - clientId: 'chicken', - name: 'core/test-block', - isValid: true, - } ); - - expect( state.present.blocks.attributes.chicken ).toEqual( { - content: 'ribs', - } ); - } ); - - it( 'should update the reusable block reference if the temporary id is swapped', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/block', - attributes: { - ref: 'random-clientId', - }, - isValid: false, - innerBlocks: [], - } ], - } ); - - const state = editor( deepFreeze( original ), { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id: 'random-clientId', - updatedId: 3, - } ); - - expect( state.present.blocks.byClientId.chicken ).toEqual( { - clientId: 'chicken', - name: 'core/block', - isValid: false, - } ); - - expect( state.present.blocks.attributes.chicken ).toEqual( { - ref: 3, - } ); - } ); - - it( 'should move the block up', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'ribs', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ 'ribs' ], - } ); - - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', '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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ movedBlock.clientId ], - rootClientId: wrapperBlock.clientId, - } ); - - expect( state.present.blocks.order ).toEqual( { - '': [ wrapperBlock.clientId ], - [ wrapperBlock.clientId ]: [ movedBlock.clientId, siblingBlock.clientId ], - [ movedBlock.clientId ]: [], - [ siblingBlock.clientId ]: [], - } ); - } ); - - it( 'should move multiple blocks up', () => { - const original = editor( 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 = editor( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ 'ribs', 'veggies' ], - } ); - - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], - rootClientId: wrapperBlock.clientId, - } ); - - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'ribs', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ 'chicken' ], - } ); - - expect( state.present.blocks.order ).toBe( original.present.blocks.order ); - } ); - - it( 'should move the block down', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'ribs', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_DOWN', - clientIds: [ 'chicken' ], - } ); - - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_DOWN', - clientIds: [ movedBlock.clientId ], - rootClientId: wrapperBlock.clientId, - } ); - - expect( state.present.blocks.order ).toEqual( { - '': [ wrapperBlock.clientId ], - [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlock.clientId ], - [ movedBlock.clientId ]: [], - [ siblingBlock.clientId ]: [], - } ); - } ); - - it( 'should move multiple blocks down', () => { - const original = editor( 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 = editor( original, { - type: 'MOVE_BLOCKS_DOWN', - clientIds: [ 'chicken', 'ribs' ], - } ); - - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ wrapperBlock ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_DOWN', - clientIds: [ movedBlockA.clientId, movedBlockB.clientId ], - rootClientId: wrapperBlock.clientId, - } ); - - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'ribs', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'MOVE_BLOCKS_DOWN', - clientIds: [ 'ribs' ], - } ); - - expect( state.present.blocks.order ).toBe( original.present.blocks.order ); - } ); - - it( 'should remove the block', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'chicken', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'ribs', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - const state = editor( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ 'chicken' ], - } ); - - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); - expect( state.present.blocks.byClientId ).toEqual( { - ribs: { - clientId: 'ribs', - name: 'core/test-block', - }, - } ); - expect( state.present.blocks.attributes ).toEqual( { - ribs: {}, - } ); - } ); - - it( 'should remove multiple blocks', () => { - const original = editor( 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 = editor( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ 'chicken', 'veggies' ], - } ); - - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); - expect( state.present.blocks.order ).not.toHaveProperty( 'veggies' ); - expect( state.present.blocks.byClientId ).toEqual( { - ribs: { - clientId: 'ribs', - name: 'core/test-block', - }, - } ); - expect( state.present.blocks.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 = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ block ], - } ); - - const state = editor( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ block.clientId ], - } ); - - expect( state.present.blocks.byClientId ).toEqual( {} ); - expect( state.present.blocks.order ).toEqual( { - '': [], - } ); - } ); - - it( 'should insert at the specified index', () => { - const original = editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'loquat', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - - const state = editor( original, { - type: 'INSERT_BLOCKS', - index: 1, - blocks: [ { - clientId: 'persimmon', - name: 'core/freeform', - innerBlocks: [], - } ], - } ); - - expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 3 ); - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); - } ); - - it( 'should move block to lower index', () => { - const original = editor( 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 = editor( original, { - type: 'MOVE_BLOCK_TO_POSITION', - clientId: 'ribs', - index: 0, - } ); - - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies' ] ); - } ); + postSavingLock, + previewLink, +} from '../reducer'; +import { INITIAL_EDITS_DEFAULTS } from '../defaults'; - it( 'should move block to higher index', () => { - const original = editor( 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 = editor( original, { - type: 'MOVE_BLOCK_TO_POSITION', - clientId: 'ribs', - index: 2, - } ); +describe( 'state', () => { + describe( 'getPostRawValue', () => { + it( 'returns original value for non-rendered content', () => { + const value = getPostRawValue( '' ); - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs' ] ); + expect( value ).toBe( '' ); } ); - it( 'should not move block if passed same index', () => { - const original = editor( 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 = editor( original, { - type: 'MOVE_BLOCK_TO_POSITION', - clientId: 'ribs', - index: 1, - } ); + it( 'returns raw value for rendered content', () => { + const value = getPostRawValue( { raw: '' } ); - expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies' ] ); + expect( value ).toBe( '' ); } ); + } ); + describe( 'editor()', () => { describe( 'edits()', () => { it( 'should save newly edited properties', () => { const original = editor( undefined, { @@ -998,7 +53,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.edits ).toEqual( { status: 'draft', title: 'post title', tags: [ 1 ], @@ -1021,7 +76,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toBe( original.present.edits ); + expect( state.edits ).toBe( original.edits ); } ); it( 'should save modified properties', () => { @@ -1042,7 +97,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.edits ).toEqual( { status: 'draft', title: 'modified title', tags: [ 2 ], @@ -1068,7 +123,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.edits ).toEqual( { meta: { a: 1, b: 2, @@ -1084,7 +139,7 @@ describe( 'state', () => { edits: {}, } ); - expect( state.present.edits ).toBe( original.present.edits ); + expect( state.edits ).toBe( original.edits ); } ); it( 'unset reset post values which match by canonical value', () => { @@ -1104,7 +159,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( {} ); + expect( state.edits ).toEqual( {} ); } ); it( 'unset reset post values by deep match', () => { @@ -1130,7 +185,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( {} ); + expect( state.edits ).toEqual( {} ); } ); it( 'should omit content when resetting', () => { @@ -1145,7 +200,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toHaveProperty( 'content' ); + expect( state.edits ).toHaveProperty( 'content' ); state = editor( original, { type: 'RESET_BLOCKS', @@ -1162,745 +217,130 @@ describe( 'state', () => { } ], } ); - expect( state.present.edits ).not.toHaveProperty( 'content' ); - } ); - } ); - - describe( 'blocks', () => { - it( 'should not reset any blocks that are not in the post', () => { - const actions = [ - { - type: 'RESET_BLOCKS', - blocks: [ - { - clientId: 'block1', - innerBlocks: [ - { clientId: 'block11', innerBlocks: [] }, - { clientId: 'block12', innerBlocks: [] }, - ], - }, - ], - }, - { - type: 'RECEIVE_BLOCKS', - blocks: [ - { - clientId: 'block2', - innerBlocks: [ - { clientId: 'block21', innerBlocks: [] }, - { clientId: 'block22', innerBlocks: [] }, - ], - }, - ], - }, - ]; - const original = deepFreeze( actions.reduce( editor, undefined ) ); - - const state = editor( original, { - type: 'RESET_BLOCKS', - blocks: [ - { - clientId: 'block3', - innerBlocks: [ - { clientId: 'block31', innerBlocks: [] }, - { clientId: 'block32', innerBlocks: [] }, - ], - }, - ], - } ); - - expect( state.present.blocks.byClientId ).toEqual( { - block2: { clientId: 'block2' }, - block21: { clientId: 'block21' }, - block22: { clientId: 'block22' }, - block3: { clientId: 'block3' }, - block31: { clientId: 'block31' }, - block32: { clientId: 'block32' }, - } ); - } ); - - describe( 'byClientId', () => { - it( 'should ignore updates to non-existent block', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); - - expect( state.present.blocks.byClientId ).toBe( original.present.blocks.byClientId ); - } ); - - it( 'should return with same reference if no changes in updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: { - updated: true, - }, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); - - expect( state.present.blocks.byClientId ).toBe( state.present.blocks.byClientId ); - } ); - } ); - - describe( 'attributes', () => { - it( 'should return with attribute block updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: {}, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); - - expect( state.present.blocks.attributes.kumquat.updated ).toBe( true ); - } ); - - it( 'should accumulate attribute block updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: { - updated: true, - }, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - moreUpdated: true, - }, - } ); - - expect( state.present.blocks.attributes.kumquat ).toEqual( { - updated: true, - moreUpdated: true, - } ); - } ); - - it( 'should ignore updates to non-existent block', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); - - expect( state.present.blocks.attributes ).toBe( original.present.blocks.attributes ); - } ); - - it( 'should return with same reference if no changes in updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: { - updated: true, - }, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); - - expect( state.present.blocks.attributes ).toBe( state.present.blocks.attributes ); - } ); - } ); - } ); - - describe( 'withHistory', () => { - it( 'should overwrite present history if updating same attributes', () => { - let state; - - state = editor( state, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: {}, - innerBlocks: [], - } ], - } ); - - expect( state.past ).toHaveLength( 1 ); - - state = editor( state, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - test: 1, - }, - } ); - - state = editor( state, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - test: 2, - }, - } ); - - expect( state.past ).toHaveLength( 2 ); - } ); - - it( 'should not overwrite present history if updating different attributes', () => { - let state; - - state = editor( state, { - type: 'RESET_BLOCKS', - blocks: [ { - clientId: 'kumquat', - attributes: {}, - innerBlocks: [], - } ], - } ); - - expect( state.past ).toHaveLength( 1 ); - - state = editor( state, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - test: 1, - }, - } ); - - state = editor( state, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - other: 1, - }, - } ); - - expect( state.past ).toHaveLength( 3 ); - } ); - } ); - } ); - - describe( 'initialEdits', () => { - it( 'should default to initial edits', () => { - const state = initialEdits( undefined, {} ); - - expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); - } ); - - it( 'should return initial edits on post reset', () => { - const state = initialEdits( undefined, { - type: 'RESET_POST', - } ); - - expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); - } ); - - it( 'should return referentially equal state if setup includes no edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should return referentially equal state if reset while having made no edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'RESET_POST', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should return setup edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR', - edits: { - title: '', - content: '', - }, - } ); - - expect( state ).toEqual( { - title: '', - content: '', - } ); - } ); - - it( 'should unset content on editor setup', () => { - const original = initialEdits( undefined, { - type: 'SETUP_EDITOR', - edits: { - title: '', - content: '', - }, - } ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR_STATE', - } ); - - expect( state ).toEqual( { title: '' } ); - } ); - - it( 'should unset values on post update', () => { - const original = initialEdits( undefined, { - type: 'SETUP_EDITOR', - edits: { - title: '', - }, - } ); - const state = initialEdits( deepFreeze( original ), { - type: 'UPDATE_POST', - edits: { - title: '', - }, - } ); - - expect( state ).toEqual( {} ); - } ); - } ); - - describe( 'currentPost()', () => { - it( 'should reset a post object', () => { - const original = deepFreeze( { title: 'unmodified' } ); - - const state = currentPost( original, { - type: 'RESET_POST', - post: { - title: 'new post', - }, - } ); - - expect( state ).toEqual( { - title: 'new post', - } ); - } ); - - it( 'should update the post object with UPDATE_POST', () => { - const original = deepFreeze( { title: 'unmodified', status: 'publish' } ); - - const state = currentPost( original, { - type: 'UPDATE_POST', - edits: { - title: 'updated post object from server', - }, - } ); - - expect( state ).toEqual( { - title: 'updated post object from server', - status: 'publish', - } ); - } ); - } ); - - 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( 'isCaretWithinFormattedText()', () => { - it( 'should set the flag to true', () => { - const state = isCaretWithinFormattedText( false, { - type: 'ENTER_FORMATTED_TEXT', - } ); - - expect( state ).toBe( true ); - } ); - - it( 'should set the flag to false', () => { - const state = isCaretWithinFormattedText( true, { - type: 'EXIT_FORMATTED_TEXT', + expect( state.edits ).not.toHaveProperty( 'content' ); } ); - - expect( state ).toBe( false ); } ); } ); - describe( 'blockSelection()', () => { - it( 'should return with block clientId as selected', () => { - const state = blockSelection( undefined, { - type: 'SELECT_BLOCK', - clientId: 'kumquat', - initialPosition: -1, - } ); - - expect( state ).toEqual( { - start: 'kumquat', - end: 'kumquat', - initialPosition: -1, - isMultiSelecting: false, - isEnabled: true, - } ); - } ); - - it( 'should set multi selection', () => { - const original = deepFreeze( { isMultiSelecting: false } ); - const state = blockSelection( original, { - type: 'MULTI_SELECT', - start: 'ribs', - end: 'chicken', - } ); - - expect( state ).toEqual( { - start: 'ribs', - end: 'chicken', - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should set continuous multi selection', () => { - const original = deepFreeze( { isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'MULTI_SELECT', - start: 'ribs', - end: 'chicken', - } ); + describe( 'initialEdits', () => { + it( 'should default to initial edits', () => { + const state = initialEdits( undefined, {} ); - expect( state ).toEqual( { - start: 'ribs', - end: 'chicken', - initialPosition: null, - isMultiSelecting: true, - } ); + expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); } ); - it( 'should start multi selection', () => { - const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: false } ); - const state = blockSelection( original, { - type: 'START_MULTI_SELECT', + it( 'should return initial edits on post reset', () => { + const state = initialEdits( undefined, { + type: 'RESET_POST', } ); - expect( state ).toEqual( { - start: 'ribs', - end: 'ribs', - initialPosition: null, - isMultiSelecting: true, - } ); + expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); } ); - it( 'should return same reference if already multi-selecting', () => { - const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'START_MULTI_SELECT', + it( 'should return referentially equal state if setup includes no edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR', } ); expect( state ).toBe( original ); } ); - it( 'should end multi selection with selection', () => { - const original = deepFreeze( { start: 'ribs', end: 'chicken', isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'STOP_MULTI_SELECT', - } ); - - expect( state ).toEqual( { - start: 'ribs', - end: 'chicken', - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should return same reference if already ended multi-selecting', () => { - const original = deepFreeze( { start: 'ribs', end: 'chicken', isMultiSelecting: false } ); - const state = blockSelection( original, { - type: 'STOP_MULTI_SELECT', + it( 'should return referentially equal state if reset while having made no edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'RESET_POST', } ); expect( state ).toBe( original ); } ); - it( 'should end multi selection without selection', () => { - const original = deepFreeze( { start: 'ribs', end: 'ribs', isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'STOP_MULTI_SELECT', + it( 'should return setup edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR', + edits: { + title: '', + content: '', + }, } ); expect( state ).toEqual( { - start: 'ribs', - end: 'ribs', - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should not update the state if the block is already selected', () => { - const original = deepFreeze( { start: 'ribs', end: 'ribs' } ); - - const state1 = blockSelection( original, { - type: 'SELECT_BLOCK', - clientId: 'ribs', - } ); - - expect( state1 ).toBe( original ); - } ); - - it( 'should unset multi selection', () => { - const original = deepFreeze( { start: 'ribs', end: 'chicken' } ); - - const state1 = blockSelection( original, { - type: 'CLEAR_SELECTED_BLOCK', - } ); - - expect( state1 ).toEqual( { - start: null, - end: null, - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should return same reference if clearing selection but no selection', () => { - const original = deepFreeze( { start: null, end: null, isMultiSelecting: false } ); - - const state1 = blockSelection( original, { - type: 'CLEAR_SELECTED_BLOCK', + title: '', + content: '', } ); - - expect( state1 ).toBe( original ); } ); - it( 'should select inserted block', () => { - const original = deepFreeze( { start: 'ribs', end: 'chicken' } ); - - const state3 = blockSelection( original, { - type: 'INSERT_BLOCKS', - blocks: [ { - clientId: 'ribs', - name: 'core/freeform', - } ], - updateSelection: true, - } ); - - expect( state3 ).toEqual( { - start: 'ribs', - end: 'ribs', - initialPosition: null, - isMultiSelecting: false, + it( 'should unset content on editor setup', () => { + const original = initialEdits( undefined, { + type: 'SETUP_EDITOR', + edits: { + title: '', + content: '', + }, } ); - } ); - - it( 'should not select inserted block if updateSelection flag is false', () => { - const original = deepFreeze( { start: 'a', end: 'b' } ); - - const state3 = blockSelection( original, { - type: 'INSERT_BLOCKS', - blocks: [ { - clientId: 'ribs', - name: 'core/freeform', - } ], - updateSelection: false, + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR_STATE', } ); - expect( state3 ).toEqual( { - start: 'a', - end: 'b', - } ); + expect( state ).toEqual( { title: '' } ); } ); - it( 'should not update the state if the block moved is already selected', () => { - const original = deepFreeze( { start: 'ribs', end: 'ribs' } ); - const state = blockSelection( original, { - type: 'MOVE_BLOCKS_UP', - clientIds: [ 'ribs' ], + it( 'should unset values on post update', () => { + const original = initialEdits( undefined, { + type: 'SETUP_EDITOR', + edits: { + title: '', + }, } ); - - expect( state ).toBe( original ); - } ); - - it( 'should replace the selected block', () => { - const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); - const state = blockSelection( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [ { - clientId: 'wings', - name: 'core/freeform', - } ], + const state = initialEdits( deepFreeze( original ), { + type: 'UPDATE_POST', + edits: { + title: '', + }, } ); - expect( state ).toEqual( { - start: 'wings', - end: 'wings', - initialPosition: null, - isMultiSelecting: false, - } ); + expect( state ).toEqual( {} ); } ); + } ); - it( 'should not replace the selected block if we keep it when replacing blocks', () => { - const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); - const state = blockSelection( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [ - { - clientId: 'chicken', - name: 'core/freeform', - }, - { - clientId: 'wings', - name: 'core/freeform', - } ], - } ); - - expect( state ).toBe( original ); - } ); + describe( 'currentPost()', () => { + it( 'should reset a post object', () => { + const original = deepFreeze( { title: 'unmodified' } ); - it( 'should reset if replacing with empty set', () => { - const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); - const state = blockSelection( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'chicken' ], - blocks: [], + const state = currentPost( original, { + type: 'RESET_POST', + post: { + title: 'new post', + }, } ); expect( state ).toEqual( { - start: null, - end: null, - initialPosition: null, - isMultiSelecting: false, + title: 'new post', } ); } ); - it( 'should keep the selected block', () => { - const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); - const state = blockSelection( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'ribs' ], - blocks: [ { - clientId: 'wings', - name: 'core/freeform', - } ], - } ); - - expect( state ).toBe( original ); - } ); + it( 'should update the post object with UPDATE_POST', () => { + const original = deepFreeze( { title: 'unmodified', status: 'publish' } ); - it( 'should remove the selection if we are removing the selected block', () => { - const original = deepFreeze( { - start: 'chicken', - end: 'chicken', - initialPosition: null, - isMultiSelecting: false, - } ); - const state = blockSelection( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ 'chicken' ], + const state = currentPost( original, { + type: 'UPDATE_POST', + edits: { + title: 'updated post object from server', + }, } ); expect( state ).toEqual( { - start: null, - end: null, - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should keep the selection if we are not removing the selected block', () => { - const original = deepFreeze( { - start: 'chicken', - end: 'chicken', - initialPosition: null, - isMultiSelecting: false, - } ); - const state = blockSelection( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ 'ribs' ], + title: 'updated post object from server', + status: 'publish', } ); - - expect( state ).toBe( original ); } ); } ); describe( 'preferences()', () => { it( 'should apply all defaults', () => { const state = preferences( undefined, {} ); - expect( state ).toEqual( { - insertUsage: {}, isPublishSidebarEnabled: true, } ); } ); @@ -1922,84 +362,6 @@ describe( 'state', () => { expect( state.isPublishSidebarEnabled ).toBe( true ); } ); - - it( 'should record recently used blocks', () => { - const state = preferences( deepFreeze( { insertUsage: {} } ), { - type: 'INSERT_BLOCKS', - blocks: [ { - clientId: 'bacon', - name: 'core-embed/twitter', - } ], - time: 123456, - } ); - - expect( state ).toEqual( { - insertUsage: { - 'core-embed/twitter': { - time: 123456, - count: 1, - insert: { name: 'core-embed/twitter' }, - }, - }, - } ); - - const twoRecentBlocks = preferences( deepFreeze( { - insertUsage: { - 'core-embed/twitter': { - time: 123456, - count: 1, - insert: { name: 'core-embed/twitter' }, - }, - }, - } ), { - type: 'INSERT_BLOCKS', - blocks: [ { - clientId: 'eggs', - name: 'core-embed/twitter', - }, { - clientId: 'bacon', - name: 'core/block', - attributes: { ref: 123 }, - } ], - time: 123457, - } ); - - expect( twoRecentBlocks ).toEqual( { - insertUsage: { - 'core-embed/twitter': { - time: 123457, - count: 2, - insert: { name: 'core-embed/twitter' }, - }, - 'core/block/123': { - time: 123457, - count: 1, - insert: { name: 'core/block', ref: 123 }, - }, - }, - } ); - } ); - - it( 'should remove recorded reusable blocks that are deleted', () => { - const initialState = { - insertUsage: { - 'core/block/123': { - time: 1000, - count: 1, - insert: { name: 'core/block', ref: 123 }, - }, - }, - }; - - const state = preferences( deepFreeze( initialState ), { - type: 'REMOVE_REUSABLE_BLOCK', - id: 123, - } ); - - expect( state ).toEqual( { - insertUsage: {}, - } ); - } ); } ); describe( 'saving()', () => { @@ -2047,28 +409,6 @@ describe( 'state', () => { } ); } ); - describe( 'blocksMode', () => { - it( 'should set mode to html if not set', () => { - const action = { - type: 'TOGGLE_BLOCK_MODE', - clientId: 'chicken', - }; - const value = blocksMode( deepFreeze( {} ), action ); - - expect( value ).toEqual( { chicken: 'html' } ); - } ); - - it( 'should toggle mode to visual if set as html', () => { - const action = { - type: 'TOGGLE_BLOCK_MODE', - clientId: 'chicken', - }; - const value = blocksMode( deepFreeze( { chicken: 'html' } ), action ); - - expect( value ).toEqual( { chicken: 'visual' } ); - } ); - } ); - describe( 'reusableBlocks()', () => { it( 'should start out empty', () => { const state = reusableBlocks( undefined, {} ); @@ -2322,153 +662,6 @@ describe( 'state', () => { } ); } ); - describe( 'template', () => { - it( 'should default to visible', () => { - const state = template( undefined, {} ); - - expect( state ).toEqual( { isValid: true } ); - } ); - - it( 'should reset the validity flag', () => { - const original = deepFreeze( { isValid: false, template: [] } ); - const state = template( original, { - type: 'SET_TEMPLATE_VALIDITY', - isValid: true, - } ); - - expect( state ).toEqual( { isValid: true, template: [] } ); - } ); - } ); - - describe( 'blockListSettings', () => { - it( 'should add new settings', () => { - const original = deepFreeze( {} ); - - const state = blockListSettings( original, { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - settings: { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - - expect( state ).toEqual( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - } ); - - it( 'should return same reference if updated as the same', () => { - const original = deepFreeze( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - - const state = blockListSettings( original, { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - settings: { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should return same reference if updated settings not assigned and id not exists', () => { - const original = deepFreeze( {} ); - - const state = blockListSettings( original, { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should update the settings of a block', () => { - const original = deepFreeze( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - allowedBlocks: true, - }, - } ); - - const state = blockListSettings( original, { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - settings: { - allowedBlocks: [ 'core/list' ], - }, - } ); - - expect( state ).toEqual( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/list' ], - }, - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - allowedBlocks: true, - }, - } ); - } ); - - it( 'should remove existing settings if updated settings not assigned', () => { - const original = deepFreeze( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - - const state = blockListSettings( original, { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', - } ); - - expect( state ).toEqual( {} ); - } ); - - it( 'should remove the settings of a block when it is replaced', () => { - const original = deepFreeze( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - allowedBlocks: true, - }, - } ); - - const state = blockListSettings( original, { - type: 'REPLACE_BLOCKS', - clientIds: [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], - } ); - - expect( state ).toEqual( { - '9db792c6-a25a-495d-adbd-97d56a4c4189': { - allowedBlocks: [ 'core/paragraph' ], - }, - } ); - } ); - - it( 'should remove the settings of a block when it is removed', () => { - const original = deepFreeze( { - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - allowedBlocks: true, - }, - } ); - - const state = blockListSettings( original, { - type: 'REMOVE_BLOCKS', - clientIds: [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], - } ); - - expect( state ).toEqual( {} ); - } ); - } ); - describe( 'autosave', () => { it( 'returns null by default', () => { const state = autosave( undefined, {} ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 6e8f963129dc7..71b905f98bec4 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { filter, without, omit } from 'lodash'; +import { filter, without } from 'lodash'; /** * WordPress dependencies @@ -13,6 +13,7 @@ import { getDefaultBlockName, setDefaultBlockName, setFreeformContentHandlerName, + getBlockTypes, } from '@wordpress/blocks'; import { RawHTML } from '@wordpress/element'; @@ -23,9 +24,6 @@ import * as selectors from '../selectors'; import { PREFERENCES_DEFAULTS } from '../defaults'; const { - canUserUseUnfilteredHTML, - hasEditorUndo, - hasEditorRedo, isEditedPostNew, hasChangedContent, isEditedPostDirty, @@ -49,69 +47,27 @@ const { isEditedPostEmpty, isEditedPostBeingScheduled, isEditedPostDateFloating, - getBlockDependantsCacheBust, - getBlockName, - getBlock, - getBlocks, - getBlockCount, - getClientIdsWithDescendants, - getClientIdsOfDescendants, - hasSelectedBlock, - getSelectedBlock, - getSelectedBlockClientId, - getBlockRootClientId, - getBlockHierarchyRootClientId, getCurrentPostAttribute, getEditedPostAttribute, getAutosaveAttribute, - getGlobalBlockCount, - getMultiSelectedBlockClientIds, - getMultiSelectedBlocks, - getMultiSelectedBlocksStartClientId, - getMultiSelectedBlocksEndClientId, - getBlockOrder, - getBlockIndex, - getPreviousBlockClientId, - getNextBlockClientId, - isBlockSelected, - hasSelectedInnerBlock, - isBlockWithinSelection, - hasMultiSelection, - isBlockMultiSelected, - isFirstMultiSelectedBlock, - getBlockMode, - isTyping, - isCaretWithinFormattedText, - getBlockInsertionPoint, - isBlockInsertionPointVisible, isSavingPost, didPostSaveRequestSucceed, didPostSaveRequestFail, getSuggestedPostFormat, - getBlocksForSerialization, getEditedPostContent, __experimentalGetReusableBlock: getReusableBlock, __experimentalIsSavingReusableBlock: isSavingReusableBlock, __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, - isSelectionEnabled, __experimentalGetReusableBlocks: getReusableBlocks, getStateBeforeOptimisticTransaction, isPublishingPost, isPublishSidebarEnabled, - canInsertBlockType, - getInserterItems, - isValidTemplate, - getTemplate, - getTemplateLock, - getBlockListSettings, POST_UPDATE_TRANSACTION_ID, isPermalinkEditable, getPermalink, getPermalinkParts, - INSERTER_UTILITY_HIGH, - INSERTER_UTILITY_MEDIUM, - INSERTER_UTILITY_LOW, isPostSavingLocked, + canUserUseUnfilteredHTML, } = selectors; describe( 'selectors', () => { @@ -201,54 +157,6 @@ describe( 'selectors', () => { setDefaultBlockName( undefined ); } ); - describe( 'hasEditorUndo', () => { - it( 'should return true when the past history is not empty', () => { - const state = { - editor: { - past: [ - {}, - ], - }, - }; - - expect( hasEditorUndo( state ) ).toBe( true ); - } ); - - it( 'should return false when the past history is empty', () => { - const state = { - editor: { - past: [], - }, - }; - - expect( hasEditorUndo( state ) ).toBe( false ); - } ); - } ); - - describe( 'hasEditorRedo', () => { - it( 'should return true when the future history is not empty', () => { - const state = { - editor: { - future: [ - {}, - ], - }, - }; - - expect( hasEditorRedo( state ) ).toBe( true ); - } ); - - it( 'should return false when the future history is empty', () => { - const state = { - editor: { - future: [], - }, - }; - - expect( hasEditorRedo( state ) ).toBe( false ); - } ); - } ); - describe( 'isEditedPostNew', () => { it( 'should return true when the post is new', () => { const state = { @@ -291,12 +199,11 @@ describe( 'selectors', () => { it( 'should return false if no dirty blocks nor content property edit', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, }; @@ -306,12 +213,11 @@ describe( 'selectors', () => { it( 'should return true if dirty blocks', () => { const state = { editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, }; @@ -321,13 +227,12 @@ describe( 'selectors', () => { it( 'should return true if content property edit', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: { - content: 'text mode edited', - }, + blocks: { + isDirty: false, + value: [], + }, + edits: { + content: 'text mode edited', }, }, }; @@ -341,12 +246,11 @@ describe( 'selectors', () => { const state = { optimist: [], editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, }; @@ -357,12 +261,11 @@ describe( 'selectors', () => { const state = { optimist: [], editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, }; @@ -373,13 +276,12 @@ describe( 'selectors', () => { const state = { optimist: [], editor: { - present: { - blocks: { - isDirty: false, - }, - edits: { - excerpt: 'hello world', - }, + blocks: { + isDirty: false, + value: [], + }, + edits: { + excerpt: 'hello world', }, }, }; @@ -393,23 +295,21 @@ describe( 'selectors', () => { { beforeState: { editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, }, }, ], editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, }; @@ -421,12 +321,11 @@ describe( 'selectors', () => { it( 'should return true when the post is not dirty and has not been saved before', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { id: 1, @@ -443,12 +342,11 @@ describe( 'selectors', () => { it( 'should return false when the post is not dirty but the post has been saved', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { id: 1, @@ -465,12 +363,11 @@ describe( 'selectors', () => { it( 'should return false when the post is dirty but the post has not been saved', () => { const state = { editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, currentPost: { id: 1, @@ -546,9 +443,7 @@ describe( 'selectors', () => { const state = { currentPost: { slug: 'post slug' }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -560,10 +455,8 @@ describe( 'selectors', () => { const state = { currentPost: { slug: 'old slug' }, editor: { - present: { - edits: { - slug: 'new slug', - }, + edits: { + slug: 'new slug', }, }, initialEdits: {}, @@ -578,9 +471,7 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - present: { - edits: { status: 'private' }, - }, + edits: { status: 'private' }, }, initialEdits: {}, }; @@ -594,9 +485,7 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - present: { - edits: { title: 'youcha' }, - }, + edits: { title: 'youcha' }, }, initialEdits: {}, }; @@ -608,9 +497,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -627,11 +514,9 @@ describe( 'selectors', () => { }, }, editor: { - present: { - edits: { - meta: { - b: 2, - }, + edits: { + meta: { + b: 2, }, }, }, @@ -749,9 +634,7 @@ describe( 'selectors', () => { it( 'should return the post edits', () => { const state = { editor: { - present: { - edits: { title: 'terga' }, - }, + edits: { title: 'terga' }, }, initialEdits: {}, }; @@ -762,9 +645,7 @@ describe( 'selectors', () => { it( 'should return value from initial edits', () => { const state = { editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: { title: 'terga' }, }; @@ -775,9 +656,7 @@ describe( 'selectors', () => { it( 'should prefer value from edits over initial edits', () => { const state = { editor: { - present: { - edits: { title: 'werga' }, - }, + edits: { title: 'werga' }, }, initialEdits: { title: 'terga' }, }; @@ -808,9 +687,7 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -824,9 +701,7 @@ describe( 'selectors', () => { status: 'private', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -841,9 +716,7 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -858,11 +731,9 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - present: { - edits: { - status: 'private', - password: null, - }, + edits: { + status: 'private', + password: null, }, }, initialEdits: {}, @@ -991,12 +862,11 @@ describe( 'selectors', () => { it( 'should return true for pending posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { status: 'pending', @@ -1012,12 +882,11 @@ describe( 'selectors', () => { it( 'should return true for draft posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { status: 'draft', @@ -1033,12 +902,11 @@ describe( 'selectors', () => { it( 'should return false for published posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { status: 'publish', @@ -1054,12 +922,11 @@ describe( 'selectors', () => { it( 'should return true for published, dirty posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, currentPost: { status: 'publish', @@ -1075,12 +942,11 @@ describe( 'selectors', () => { it( 'should return false for private posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { status: 'private', @@ -1096,12 +962,11 @@ describe( 'selectors', () => { it( 'should return false for scheduled posts', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, currentPost: { status: 'future', @@ -1120,12 +985,11 @@ describe( 'selectors', () => { status: 'private', }, editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + isDirty: true, + value: [], }, + edits: {}, }, saving: { requesting: false, @@ -1162,14 +1026,10 @@ describe( 'selectors', () => { it( 'should return false if the post has no title, excerpt, content', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1182,14 +1042,10 @@ describe( 'selectors', () => { it( 'should return false if the post has a title but save already in progress', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1206,14 +1062,10 @@ describe( 'selectors', () => { it( 'should return true if the post has a title', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1228,14 +1080,13 @@ describe( 'selectors', () => { it( 'should return true if the post has an excerpt', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + byClientId: {}, + attributes: {}, + order: {}, + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1250,26 +1101,19 @@ describe( 'selectors', () => { it( 'should return true if the post has content', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-block-a', - isValid: true, - }, - }, - attributes: { - 123: { + blocks: { + value: [ + { + clientId: 123, + name: 'core/test-block-a', + isValid: true, + attributes: { text: '', }, }, - order: { - '': [ 123 ], - }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1282,25 +1126,19 @@ describe( 'selectors', () => { it( 'should return false if the post has no title, excerpt and empty classic block', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - }, - }, - attributes: { - 123: { + blocks: { + value: [ + { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { content: '', }, }, - order: { - '': [ 123 ], - }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1313,26 +1151,19 @@ describe( 'selectors', () => { it( 'should return true if the post has a title and empty classic block', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - isValid: true, - }, - }, - attributes: { - 123: { + blocks: { + value: [ + { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { content: '', }, }, - order: { - '': [ 123 ], - }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1349,14 +1180,10 @@ describe( 'selectors', () => { it( 'should return false if the post is not saveable', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1376,14 +1203,10 @@ describe( 'selectors', () => { it( 'should return true if there is not yet an autosave', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1399,12 +1222,11 @@ describe( 'selectors', () => { it( 'should return false if none of title, excerpt, or content have changed', () => { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + value: [], + isDirty: false, }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1424,12 +1246,11 @@ describe( 'selectors', () => { it( 'should return true if content has changes', () => { const state = { editor: { - present: { - blocks: { - isDirty: true, - }, - edits: {}, + blocks: { + value: [], + isDirty: true, }, + edits: {}, }, currentPost: { title: 'foo', @@ -1450,12 +1271,11 @@ describe( 'selectors', () => { for ( const constantField of without( [ 'title', 'excerpt' ], variantField ) ) { const state = { editor: { - present: { - blocks: { - isDirty: false, - }, - edits: {}, + blocks: { + isDirty: false, + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1522,14 +1342,10 @@ describe( 'selectors', () => { it( 'should return true if no blocks and no content', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1541,26 +1357,17 @@ describe( 'selectors', () => { it( 'should return false if blocks', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-block-a', - isValid: true, - }, - }, + blocks: { + value: [ { + clientId: 123, + name: 'core/test-block-a', + isValid: true, attributes: { - 123: { - text: '', - }, - }, - order: { - '': [ 123 ], + text: '', }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1577,25 +1384,16 @@ describe( 'selectors', () => { // See: https://github.com/WordPress/gutenberg/pull/13086 const state = { editor: { - present: { - blocks: { - byClientId: { - block1: { - clientId: 'block1', - name: 'core/test-default', - }, - }, + blocks: { + value: [ { + clientId: 'block1', + name: 'core/test-default', attributes: { - block1: { - modified: false, - }, + modified: false, }, - order: { - '': [ 'block1' ], - }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1607,27 +1405,18 @@ describe( 'selectors', () => { it( 'should return true if blocks, but empty content edit', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-block-a', - isValid: true, - }, - }, + blocks: { + value: [ { + clientId: 123, + name: 'core/test-block-a', + isValid: true, attributes: { - 123: { - text: '', - }, + text: '', }, - order: { - '': [ 123 ], - }, - }, - edits: { - content: '', - }, + } ], + }, + edits: { + content: '', }, }, initialEdits: {}, @@ -1642,14 +1431,10 @@ describe( 'selectors', () => { it( 'should return true if the post has an empty content property', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1663,15 +1448,11 @@ describe( 'selectors', () => { it( 'should return false if edits include a non-empty content property', () => { const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: { - content: 'sassel', - }, + blocks: { + value: [], + }, + edits: { + content: 'sassel', }, }, initialEdits: {}, @@ -1684,26 +1465,17 @@ describe( 'selectors', () => { it( 'should return true if empty classic block', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - isValid: true, - }, - }, + blocks: { + value: [ { + clientId: 123, + name: 'core/test-freeform', + isValid: true, attributes: { - 123: { - content: '', - }, + content: '', }, - order: { - '': [ 123 ], - }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, currentPost: {}, @@ -1715,26 +1487,17 @@ describe( 'selectors', () => { it( 'should return true if empty content freeform block', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - isValid: true, - }, - }, + blocks: { + value: [ { + clientId: 123, + name: 'core/test-freeform', + isValid: true, attributes: { - 123: { - content: '', - }, + content: '', }, - order: { - '': [ 123 ], - }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1748,26 +1511,17 @@ describe( 'selectors', () => { it( 'should return false if non-empty content freeform block', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - isValid: true, - }, - }, + blocks: { + value: [ { + clientId: 123, + name: 'core/test-freeform', + isValid: true, attributes: { - 123: { - content: 'Test Data', - }, - }, - order: { - '': [ 123 ], + content: 'Test Data', }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1781,34 +1535,27 @@ describe( 'selectors', () => { it( 'should return false for multiple empty freeform blocks', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - 123: { - clientId: 123, - name: 'core/test-freeform', - isValid: true, - }, - 456: { - clientId: 456, - name: 'core/test-freeform', - isValid: true, - }, - }, - attributes: { - 123: { + blocks: { + value: [ + { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { content: '', }, - 456: { + }, + { + clientId: 456, + name: 'core/test-freeform', + isValid: true, + attributes: { content: '', }, }, - order: { - '': [ 123, 456 ], - }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, currentPost: { @@ -1826,9 +1573,7 @@ describe( 'selectors', () => { const date = new Date( time ); const state = { editor: { - present: { - edits: { date: date.toUTCString() }, - }, + edits: { date: date.toUTCString() }, }, initialEdits: {}, }; @@ -1839,9 +1584,7 @@ describe( 'selectors', () => { it( 'should return false for posts with an old date', () => { const state = { editor: { - present: { - edits: { date: '2016-05-30T17:21:39' }, - }, + edits: { date: '2016-05-30T17:21:39' }, }, initialEdits: {}, }; @@ -1859,9 +1602,7 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -1877,9 +1618,7 @@ describe( 'selectors', () => { status: 'auto-draft', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -1895,9 +1634,7 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -1913,9 +1650,7 @@ describe( 'selectors', () => { status: 'auto-draft', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -1931,9 +1666,7 @@ describe( 'selectors', () => { status: 'publish', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -1942,3038 +1675,354 @@ describe( 'selectors', () => { } ); } ); - describe( 'getBlockDependantsCacheBust', () => { - const rootBlock = { clientId: 123, name: 'core/paragraph' }; - const rootBlockAttributes = {}; - const rootOrder = [ 123 ]; - - it( 'returns an unchanging reference', () => { - const rootBlockOrder = []; - - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - const nextState = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - - it( 'returns a new reference on added inner block', () => { + describe( 'isSavingPost', () => { + it( 'should return true if the post is currently being saved', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - const nextState = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: {}, - }, - order: { - '': rootOrder, - 123: [ 456 ], - 456: [], - }, - }, - edits: {}, - }, + saving: { + requesting: true, }, - initialEdits: {}, }; - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + expect( isSavingPost( state ) ).toBe( true ); } ); - it( 'returns an unchanging reference on unchanging inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlock = { clientId: 456, name: 'core/paragraph' }; - const childBlockAttributes = {}; - const childBlockOrder = []; - + it( 'should return false if the post is not currently being saved', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - const nextState = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - edits: {}, - }, + saving: { + requesting: false, }, - initialEdits: {}, }; - expect( - getBlockDependantsCacheBust( state, 123 ) - ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + expect( isSavingPost( state ) ).toBe( false ); } ); + } ); - it( 'returns a new reference on updated inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlockOrder = []; - + describe( 'didPostSaveRequestSucceed', () => { + it( 'should return true if the post save request is successful', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: {}, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - const nextState = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: { content: [ 'foo' ] }, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - edits: {}, - }, + saving: { + successful: true, }, - initialEdits: {}, }; - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + expect( didPostSaveRequestSucceed( state ) ).toBe( true ); } ); - it( 'returns a new reference on updated grandchild inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlock = { clientId: 456, name: 'core/paragraph' }; - const childBlockAttributes = {}; - const childBlockOrder = [ 789 ]; - const grandChildBlockOrder = []; - + it( 'should return true if the post save request has failed', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - 789: {}, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - - const nextState = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - 789: { content: [ 'foo' ] }, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, - }, - }, - edits: {}, - }, + saving: { + successful: false, }, - initialEdits: {}, }; - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); + expect( didPostSaveRequestSucceed( state ) ).toBe( false ); } ); } ); - describe( 'getBlockName', () => { - it( 'returns null if no block by clientId', () => { + describe( 'didPostSaveRequestFail', () => { + it( 'should return true if the post save request has failed', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, - }, + saving: { + error: 'error', }, - initialEdits: {}, }; - const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); - - expect( name ).toBe( null ); + expect( didPostSaveRequestFail( state ) ).toBe( true ); } ); - it( 'returns block name', () => { + it( 'should return true if the post save request is successful', () => { const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - name: 'core/paragraph', - }, - }, - attributes: { - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, - }, - order: { - '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], - }, - }, - edits: {}, - }, + saving: { + error: false, }, - initialEdits: {}, }; - const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); - - expect( name ).toBe( 'core/paragraph' ); + expect( didPostSaveRequestFail( state ) ).toBe( false ); } ); } ); - describe( 'getBlock', () => { - it( 'should return the block', () => { + describe( 'getSuggestedPostFormat', () => { + it( 'returns null if cannot be determined', () => { const state = { - currentPost: {}, editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 123: {}, - }, - order: { - '': [ 123 ], - 123: [], - }, - }, - edits: {}, + blocks: { + value: [], }, + edits: {}, }, initialEdits: {}, - }; - - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [], - } ); - } ); - - it( 'should return null if the block is not present in state', () => { - const state = { currentPost: {}, - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, - }, - }, - initialEdits: {}, }; - expect( getBlock( state, 123 ) ).toBe( null ); + expect( getSuggestedPostFormat( state ) ).toBeNull(); } ); - it( 'should include inner blocks', () => { + it( 'returns null if there is more than one block in the post', () => { const state = { - currentPost: {}, editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/paragraph' }, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: {}, - 456: {}, + blocks: { + value: [ + { + clientId: 123, + name: 'core/image', + attributes: {}, }, - order: { - '': [ 123 ], - 123: [ 456 ], - 456: [], + { + clientId: 456, + name: 'core/quote', + attributes: {}, }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, + currentPost: {}, }; - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [ { - clientId: 456, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [], - } ], - } ); + expect( getSuggestedPostFormat( state ) ).toBeNull(); } ); - it( 'should merge meta attributes for the block', () => { - registerBlockType( 'core/meta-block', { - save: ( props ) => props.attributes.text, - category: 'common', - title: 'test block', - attributes: { - foo: { - type: 'string', - source: 'meta', - meta: 'foo', - }, - }, - } ); - + it( 'returns Image if the first block is of type `core/image`', () => { const state = { - currentPost: { - meta: { - foo: 'bar', - }, - }, editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/meta-block' }, - }, - attributes: { - 123: {}, - }, - order: { - '': [ 123 ], - 123: [], + blocks: { + value: [ + { + clientId: 123, + name: 'core/image', + attributes: {}, }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, - }; - - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/meta-block', - attributes: { - foo: 'bar', - }, - innerBlocks: [], - } ); - - unregisterBlockType( 'core/meta-block' ); - } ); - } ); - - describe( 'getBlocks', () => { - it( 'should return the ordered blocks', () => { - const state = { currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 23: {}, - 123: {}, - }, - order: { - '': [ 123, 23 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, }; - - expect( getBlocks( state ) ).toEqual( [ - { clientId: 123, name: 'core/paragraph', attributes: {}, innerBlocks: [] }, - { clientId: 23, name: 'core/heading', attributes: {}, innerBlocks: [] }, - ] ); - } ); - } ); - - describe( 'getClientIdsOfDescendants', () => { - it( 'should return the ids of any descendants, given an array of clientIds', () => { - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - '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: { - '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: { - '': [ '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' ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - expect( getClientIdsOfDescendants( state, [ 'uuid-10' ] ) ).toEqual( [ - 'uuid-12', - 'uuid-14', - 'uuid-16', - 'uuid-18', - 'uuid-24', - 'uuid-26', - 'uuid-28', - 'uuid-30', - ] ); - } ); - } ); - - describe( 'getClientIdsWithDescendants', () => { - it( 'should return the ids for top-level blocks and their descendants of any depth (for nested blocks).', () => { - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - '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: { - '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: { - '': [ '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' ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - }; - expect( getClientIdsWithDescendants( state ) ).toEqual( [ - 'uuid-6', - 'uuid-8', - 'uuid-10', - 'uuid-22', - 'uuid-12', - 'uuid-14', - 'uuid-16', - 'uuid-18', - 'uuid-24', - 'uuid-26', - 'uuid-28', - 'uuid-30', - ] ); - } ); - } ); - - describe( 'getBlockCount', () => { - it( 'should return the number of top-level blocks in the post', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 23: {}, - 123: {}, - }, - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getBlockCount( state ) ).toBe( 2 ); - } ); - - it( 'should return the number of blocks in a nested context', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/columns' }, - 456: { clientId: 456, name: 'core/paragraph' }, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: {}, - 456: {}, - 789: {}, - }, - order: { - '': [ 123 ], - 123: [ 456, 789 ], - }, - }, - }, - }, - }; - - expect( getBlockCount( state, '123' ) ).toBe( 2 ); - } ); - } ); - - describe( 'hasSelectedBlock', () => { - it( 'should return false if no selection', () => { - const state = { - blockSelection: { - start: null, - end: null, - }, - }; - - expect( hasSelectedBlock( state ) ).toBe( false ); - } ); - - it( 'should return false if multi-selection', () => { - const state = { - blockSelection: { - start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - end: '9db792c6-a25a-495d-adbd-97d56a4c4189', - }, - }; - - expect( hasSelectedBlock( state ) ).toBe( false ); - } ); - - it( 'should return true if singular selection', () => { - const state = { - blockSelection: { - start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - }, - }; - - expect( hasSelectedBlock( state ) ).toBe( true ); - } ); - } ); - - describe( 'getGlobalBlockCount', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/heading' }, - 456: { clientId: 456, name: 'core/paragraph' }, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: {}, - 456: {}, - 789: {}, - }, - order: { - '': [ 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 = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - }, - }, - }; - expect( getGlobalBlockCount( emptyState ) ).toBe( 0 ); - expect( getGlobalBlockCount( emptyState, 'core/heading' ) ).toBe( 0 ); - } ); - } ); - - describe( 'getSelectedBlockClientId', () => { - it( 'should return null if no block is selected', () => { - const state = { - blockSelection: { start: null, end: null }, - }; - - expect( getSelectedBlockClientId( state ) ).toBe( null ); - } ); - - it( 'should return null if there is multi selection', () => { - const state = { - blockSelection: { start: 23, end: 123 }, - }; - - expect( getSelectedBlockClientId( state ) ).toBe( null ); - } ); - - it( 'should return the selected block ClientId', () => { - const state = { - editor: { present: { blocks: { byClientId: { 23: { name: 'fake block' } } } } }, - blockSelection: { start: 23, end: 23 }, - }; - - expect( getSelectedBlockClientId( state ) ).toEqual( 23 ); - } ); - } ); - - describe( 'getSelectedBlock', () => { - it( 'should return null if no block is selected', () => { - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 23: {}, - 123: {}, - }, - order: { - '': [ 23, 123 ], - 23: [], - 123: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - blockSelection: { start: null, end: null }, - }; - - expect( getSelectedBlock( state ) ).toBe( null ); - } ); - - it( 'should return null if there is multi selection', () => { - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 23: {}, - 123: {}, - }, - order: { - '': [ 23, 123 ], - 23: [], - 123: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - blockSelection: { start: 23, end: 123 }, - }; - - expect( getSelectedBlock( state ) ).toBe( null ); - } ); - - it( 'should return the selected block', () => { - const state = { - currentPost: {}, - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - 123: { clientId: 123, name: 'core/paragraph' }, - }, - attributes: { - 23: {}, - 123: {}, - }, - order: { - '': [ 23, 123 ], - 23: [], - 123: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - blockSelection: { start: 23, end: 23 }, - }; - - expect( getSelectedBlock( state ) ).toEqual( { - clientId: 23, - name: 'core/heading', - attributes: {}, - innerBlocks: [], - } ); - } ); - } ); - - describe( 'getBlockRootClientId', () => { - it( 'should return null if the block does not exist', () => { - const state = { - editor: { - present: { - blocks: { - order: {}, - }, - }, - }, - }; - - expect( getBlockRootClientId( state, 56 ) ).toBeNull(); - } ); - - it( 'should return root ClientId relative the block ClientId', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getBlockRootClientId( state, 56 ) ).toBe( '123' ); - } ); - } ); - - describe( 'getBlockHierarchyRootClientId', () => { - it( 'should return the given block if the block has no parents', () => { - const state = { - editor: { - present: { - blocks: { - order: {}, - }, - }, - }, - }; - - expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( 56 ); - } ); - - it( 'should return root ClientId relative the block ClientId', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( '123' ); - } ); - - it( 'should return the top level root ClientId relative the block ClientId', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ '123', '23' ], - 123: [ '456', '56' ], - 56: [ '12' ], - }, - }, - }, - }, - }; - - expect( getBlockHierarchyRootClientId( state, '12' ) ).toBe( '123' ); - } ); - } ); - - describe( 'getMultiSelectedBlockClientIds', () => { - it( 'should return empty if there is no multi selection', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - blockSelection: { start: null, end: null }, - }; - - expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [] ); - } ); - - it( 'should return selected block clientIds if there is multi selection', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - blockSelection: { start: 2, end: 4 }, - }; - - expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ 4, 3, 2 ] ); - } ); - - it( 'should return selected block clientIds if there is multi selection (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - 4: [ 9, 8, 7, 6 ], - }, - }, - }, - }, - blockSelection: { start: 7, end: 9 }, - }; - - expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [ 9, 8, 7 ] ); - } ); - } ); - - describe( 'getMultiSelectedBlocks', () => { - it( 'should return the same reference on subsequent invocations of empty selection', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, - }, - }, - initialEdits: {}, - blockSelection: { start: null, end: null }, - currentPost: {}, - }; - - expect( - getMultiSelectedBlocks( state ) - ).toBe( getMultiSelectedBlocks( state ) ); - } ); - } ); - - describe( 'getMultiSelectedBlocksStartClientId', () => { - it( 'returns null if there is no multi selection', () => { - const state = { - blockSelection: { start: null, end: null }, - }; - - expect( getMultiSelectedBlocksStartClientId( state ) ).toBeNull(); - } ); - - it( 'returns multi selection start', () => { - const state = { - blockSelection: { start: 2, end: 4 }, - }; - - expect( getMultiSelectedBlocksStartClientId( state ) ).toBe( 2 ); - } ); - } ); - - describe( 'getMultiSelectedBlocksEndClientId', () => { - it( 'returns null if there is no multi selection', () => { - const state = { - blockSelection: { start: null, end: null }, - }; - - expect( getMultiSelectedBlocksEndClientId( state ) ).toBeNull(); - } ); - - it( 'returns multi selection end', () => { - const state = { - blockSelection: { start: 2, end: 4 }, - }; - - expect( getMultiSelectedBlocksEndClientId( state ) ).toBe( 4 ); - } ); - } ); - - describe( 'getBlockOrder', () => { - it( 'should return the ordered block ClientIds of top-level blocks by default', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getBlockOrder( state ) ).toEqual( [ 123, 23 ] ); - } ); - - it( 'should return the ordered block ClientIds at a specified rootClientId', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456 ], - }, - }, - }, - }, - }; - - expect( getBlockOrder( state, '123' ) ).toEqual( [ 456 ] ); - } ); - } ); - - describe( 'getBlockIndex', () => { - it( 'should return the block order', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getBlockIndex( state, 23 ) ).toBe( 1 ); - } ); - - it( 'should return the block order (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getBlockIndex( state, 56, '123' ) ).toBe( 1 ); - } ); - } ); - - describe( 'getPreviousBlockClientId', () => { - it( 'should return the previous block', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getPreviousBlockClientId( state, 23 ) ).toEqual( 123 ); - } ); - - it( 'should return the previous block (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getPreviousBlockClientId( state, 56, '123' ) ).toEqual( 456 ); - } ); - - it( 'should return null for the first block', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getPreviousBlockClientId( state, 123 ) ).toBeNull(); - } ); - - it( 'should return null for the first block (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getPreviousBlockClientId( state, 456, '123' ) ).toBeNull(); - } ); - } ); - - describe( 'getNextBlockClientId', () => { - it( 'should return the following block', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getNextBlockClientId( state, 123 ) ).toEqual( 23 ); - } ); - - it( 'should return the following block (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getNextBlockClientId( state, 456, '123' ) ).toEqual( 56 ); - } ); - - it( 'should return null for the last block', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - }, - }, - }, - }, - }; - - expect( getNextBlockClientId( state, 23 ) ).toBeNull(); - } ); - - it( 'should return null for the last block (nested context)', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], - }, - }, - }, - }, - }; - - expect( getNextBlockClientId( state, 56, '123' ) ).toBeNull(); - } ); - } ); - - describe( 'isBlockSelected', () => { - it( 'should return true if the block is selected', () => { - const state = { - blockSelection: { start: 123, end: 123 }, - }; - - expect( isBlockSelected( state, 123 ) ).toBe( true ); - } ); - - it( 'should return false if a multi-selection range exists', () => { - const state = { - blockSelection: { start: 123, end: 124 }, - }; - - expect( isBlockSelected( state, 123 ) ).toBe( false ); - } ); - - it( 'should return false if the block is not selected', () => { - const state = { - blockSelection: { start: null, end: null }, - }; - - 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 = { - blockSelection: { start: 5, end: 5 }, - editor: { - present: { - blocks: { - order: { - 4: [ 3, 2, 1 ], - }, - }, - }, - }, - }; - - expect( hasSelectedInnerBlock( state, 4 ) ).toBe( false ); - } ); - - it( 'should return true if the selected block is a child of the given ClientId', () => { - const state = { - blockSelection: { start: 3, end: 3 }, - editor: { - present: { - blocks: { - order: { - 4: [ 3, 2, 1 ], - }, - }, - }, - }, - }; - - 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 = { - editor: { - present: { - blocks: { - order: { - 6: [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - blockSelection: { start: 2, end: 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 = { - editor: { - present: { - blocks: { - order: { - 3: [ 2, 1 ], - 6: [ 5, 4 ], - }, - }, - }, - }, - blockSelection: { start: 5, end: 4 }, - }; - expect( hasSelectedInnerBlock( state, 3 ) ).toBe( false ); - } ); - } ); - - describe( 'isBlockWithinSelection', () => { - it( 'should return true if the block is selected but not the last', () => { - const state = { - blockSelection: { start: 5, end: 3 }, - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - }; - - expect( isBlockWithinSelection( state, 4 ) ).toBe( true ); - } ); - - it( 'should return false if the block is the last selected', () => { - const state = { - blockSelection: { start: 5, end: 3 }, - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - }; - - expect( isBlockWithinSelection( state, 3 ) ).toBe( false ); - } ); - - it( 'should return false if the block is not selected', () => { - const state = { - blockSelection: { start: 5, end: 3 }, - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - }; - - expect( isBlockWithinSelection( state, 2 ) ).toBe( false ); - } ); - - it( 'should return false if there is no selection', () => { - const state = { - blockSelection: {}, - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - }; - - expect( isBlockWithinSelection( state, 4 ) ).toBe( false ); - } ); - } ); - - describe( 'hasMultiSelection', () => { - it( 'should return false if no selection', () => { - const state = { - blockSelection: { - start: null, - end: null, - }, - }; - - expect( hasMultiSelection( state ) ).toBe( false ); - } ); - - it( 'should return false if singular selection', () => { - const state = { - blockSelection: { - start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - }, - }; - - expect( hasMultiSelection( state ) ).toBe( false ); - } ); - - it( 'should return true if multi-selection', () => { - const state = { - blockSelection: { - start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - end: '9db792c6-a25a-495d-adbd-97d56a4c4189', - }, - }; - - expect( hasMultiSelection( state ) ).toBe( true ); - } ); - } ); - - describe( 'isBlockMultiSelected', () => { - const state = { - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - blockSelection: { start: 2, end: 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 = { - editor: { - present: { - blocks: { - order: { - '': [ 5, 4, 3, 2, 1 ], - }, - }, - }, - }, - blockSelection: { start: 2, end: 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( 'isCaretWithinFormattedText', () => { - it( 'returns true if the isCaretWithinFormattedText state is also true', () => { - const state = { - isCaretWithinFormattedText: true, - }; - - expect( isCaretWithinFormattedText( state ) ).toBe( true ); - } ); - - it( 'returns false if the isCaretWithinFormattedText state is also false', () => { - const state = { - isCaretWithinFormattedText: false, - }; - - expect( isCaretWithinFormattedText( state ) ).toBe( false ); - } ); - } ); - - describe( 'isSelectionEnabled', () => { - it( 'should return true if selection is enable', () => { - const state = { - blockSelection: { - isEnabled: true, - }, - }; - - expect( isSelectionEnabled( state ) ).toBe( true ); - } ); - - it( 'should return false if selection is disabled', () => { - const state = { - blockSelection: { - isEnabled: false, - }, - }; - - expect( isSelectionEnabled( state ) ).toBe( false ); - } ); - } ); - - describe( 'getBlockInsertionPoint', () => { - it( 'should return the explicitly assigned insertion point', () => { - const state = { - currentPost: {}, - preferences: { mode: 'visual' }, - blockSelection: { - start: 'clientId2', - end: 'clientId2', - }, - editor: { - present: { - blocks: { - byClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - attributes: { - clientId1: {}, - clientId2: {}, - }, - order: { - '': [ 'clientId1' ], - clientId1: [ 'clientId2' ], - clientId2: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - insertionPoint: { - rootClientId: undefined, - index: 0, - }, - }; - - expect( getBlockInsertionPoint( state ) ).toEqual( { - rootClientId: undefined, - index: 0, - } ); - } ); - - it( 'should return an object for the selected block', () => { - const state = { - currentPost: {}, - preferences: { mode: 'visual' }, - blockSelection: { - start: 'clientId1', - end: 'clientId1', - }, - editor: { - present: { - blocks: { - byClientId: { - clientId1: { clientId: 'clientId1' }, - }, - attributes: { - clientId1: {}, - }, - order: { - '': [ 'clientId1' ], - clientId1: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - insertionPoint: null, - }; - - expect( getBlockInsertionPoint( state ) ).toEqual( { - rootClientId: undefined, - index: 1, - } ); - } ); - - it( 'should return an object for the nested selected block', () => { - const state = { - currentPost: {}, - preferences: { mode: 'visual' }, - blockSelection: { - start: 'clientId2', - end: 'clientId2', - }, - editor: { - present: { - blocks: { - byClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - attributes: { - clientId1: {}, - clientId2: {}, - }, - order: { - '': [ 'clientId1' ], - clientId1: [ 'clientId2' ], - clientId2: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - insertionPoint: null, - }; - - expect( getBlockInsertionPoint( state ) ).toEqual( { - rootClientId: 'clientId1', - index: 1, - } ); - } ); - - it( 'should return an object for the last multi selected clientId', () => { - const state = { - currentPost: {}, - preferences: { mode: 'visual' }, - blockSelection: { - start: 'clientId1', - end: 'clientId2', - }, - editor: { - present: { - blocks: { - byClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - attributes: { - clientId1: {}, - clientId2: {}, - }, - order: { - '': [ 'clientId1', 'clientId2' ], - clientId1: [], - clientId2: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - insertionPoint: null, - }; - - expect( getBlockInsertionPoint( state ) ).toEqual( { - rootClientId: undefined, - index: 2, - } ); - } ); - - it( 'should return an object for the last block if no selection', () => { - const state = { - currentPost: {}, - preferences: { mode: 'visual' }, - blockSelection: { - start: null, - end: null, - }, - editor: { - present: { - blocks: { - byClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - attributes: { - clientId1: {}, - clientId2: {}, - }, - order: { - '': [ 'clientId1', 'clientId2' ], - clientId1: [], - clientId2: [], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - insertionPoint: null, - }; - - expect( getBlockInsertionPoint( state ) ).toEqual( { - rootClientId: undefined, - index: 2, - } ); - } ); - } ); - - describe( 'isBlockInsertionPointVisible', () => { - it( 'should return false if no assigned insertion point', () => { - const state = { - insertionPoint: null, - }; - - expect( isBlockInsertionPointVisible( state ) ).toBe( false ); - } ); - - it( 'should return true if assigned insertion point', () => { - const state = { - insertionPoint: { - rootClientId: undefined, - index: 5, - }, - }; - - expect( isBlockInsertionPointVisible( state ) ).toBe( true ); - } ); - } ); - - describe( 'isSavingPost', () => { - it( 'should return true if the post is currently being saved', () => { - const state = { - saving: { - requesting: true, - }, - }; - - expect( isSavingPost( state ) ).toBe( true ); - } ); - - it( 'should return false if the post is not currently being saved', () => { - const state = { - saving: { - requesting: false, - }, - }; - - expect( isSavingPost( state ) ).toBe( false ); - } ); - } ); - - describe( 'didPostSaveRequestSucceed', () => { - it( 'should return true if the post save request is successful', () => { - const state = { - saving: { - successful: true, - }, - }; - - expect( didPostSaveRequestSucceed( state ) ).toBe( true ); - } ); - - it( 'should return true if the post save request has failed', () => { - const state = { - saving: { - successful: false, - }, - }; - - expect( didPostSaveRequestSucceed( state ) ).toBe( false ); - } ); - } ); - - describe( 'didPostSaveRequestFail', () => { - it( 'should return true if the post save request has failed', () => { - const state = { - saving: { - error: 'error', - }, - }; - - expect( didPostSaveRequestFail( state ) ).toBe( true ); - } ); - - it( 'should return true if the post save request is successful', () => { - const state = { - saving: { - error: false, - }, - }; - - expect( didPostSaveRequestFail( state ) ).toBe( false ); - } ); - } ); - - describe( 'getSuggestedPostFormat', () => { - it( 'returns null if cannot be determined', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBeNull(); - } ); - - it( 'returns null if there is more than one block in the post', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/image' }, - 456: { clientId: 456, name: 'core/quote' }, - }, - attributes: { - 123: {}, - 456: {}, - }, - order: { - '': [ 123, 456 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBeNull(); - } ); - - it( 'returns Image if the first block is of type `core/image`', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/image' }, - }, - attributes: { - 123: {}, - }, - order: { - '': [ 123 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBe( 'image' ); - } ); - - it( 'returns Quote if the first block is of type `core/quote`', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 456: { clientId: 456, name: 'core/quote' }, - }, - attributes: { - 456: {}, - }, - order: { - '': [ 456 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBe( 'quote' ); - } ); - - it( 'returns Video if the first block is of type `core-embed/youtube`', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 567: { clientId: 567, name: 'core-embed/youtube' }, - }, - attributes: { - 567: {}, - }, - order: { - '': [ 567 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBe( 'video' ); - } ); - - it( 'returns Quote if the first block is of type `core/quote` and second is of type `core/paragraph`', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 456: { clientId: 456, name: 'core/quote' }, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 456: {}, - 789: {}, - }, - order: { - '': [ 456, 789 ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getSuggestedPostFormat( state ) ).toBe( 'quote' ); - } ); - } ); - - describe( 'getBlocksForSerialization', () => { - it( 'should return blocks', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { - clientId: 'block1', - name: 'core/test-default', - }, - block2: { - clientId: 'block2', - name: 'core/heading', - }, - }, - attributes: { - block1: { - modified: false, - }, - block2: {}, - }, - order: { - '': [ 'block1', 'block2' ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getBlocksForSerialization( state ) ).toEqual( [ - { clientId: 'block1', name: 'core/test-default', attributes: { modified: false }, innerBlocks: [] }, - { clientId: 'block2', name: 'core/heading', attributes: {}, innerBlocks: [] }, - ] ); - } ); - - it( 'should return an empty set if content is a single unmodified default block', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { - clientId: 'block1', - name: 'core/test-default', - }, - }, - attributes: { - block1: { - modified: false, - }, - }, - order: { - '': [ 'block1' ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getBlocksForSerialization( state ) ).toEqual( [] ); - } ); - - it( 'should return a set including a single modified default block', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { - clientId: 'block1', - name: 'core/test-default', - }, - }, - attributes: { - block1: { - modified: true, - }, - }, - order: { - '': [ 'block1' ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - expect( getBlocksForSerialization( state ) ).toEqual( [ - { clientId: 'block1', name: 'core/test-default', attributes: { modified: true }, innerBlocks: [] }, - ] ); - } ); - } ); - - describe( 'getEditedPostContent', () => { - it( 'defers to returning an edited post attribute', () => { - const block = createBlock( 'core/block' ); - - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ block.clientId ]: omit( block, 'attributes' ), - }, - attributes: { - [ block.clientId ]: block.attributes, - }, - order: { - '': [ block.clientId ], - }, - }, - edits: { - content: 'custom edit', - }, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( 'custom edit' ); - } ); - - it( 'returns serialization of blocks', () => { - const block = createBlock( 'core/block' ); - - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ block.clientId ]: omit( block, 'attributes' ), - }, - attributes: { - [ block.clientId ]: block.attributes, - }, - order: { - '': [ block.clientId ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( '' ); - } ); - - it( 'returns removep\'d serialization of blocks for single unknown', () => { - const unknownBlock = createBlock( 'core/test-freeform', { - content: '

foo

', - } ); - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ unknownBlock.clientId ]: omit( unknownBlock, 'attributes' ), - }, - attributes: { - [ unknownBlock.clientId ]: unknownBlock.attributes, - }, - order: { - '': [ unknownBlock.clientId ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( 'foo' ); - } ); - - it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => { - const firstUnknown = createBlock( 'core/test-freeform', { - content: '

foo

', - } ); - const secondUnknown = createBlock( 'core/test-freeform', { - content: '

bar

', - } ); - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ firstUnknown.clientId ]: omit( firstUnknown, 'attributes' ), - [ secondUnknown.clientId ]: omit( secondUnknown, 'attributes' ), - }, - attributes: { - [ firstUnknown.clientId ]: firstUnknown.attributes, - [ secondUnknown.clientId ]: secondUnknown.attributes, - }, - order: { - '': [ firstUnknown.clientId, secondUnknown.clientId ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( '

foo

\n\n

bar

' ); - } ); - - it( 'returns empty string for single unmodified default block', () => { - const defaultBlock = createBlock( getDefaultBlockName() ); - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ defaultBlock.clientId ]: omit( defaultBlock, 'attributes' ), - }, - attributes: { - [ defaultBlock.clientId ]: defaultBlock.attributes, - }, - order: { - '': [ defaultBlock.clientId ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( '' ); - } ); - - it( 'should not return empty string for modified default block', () => { - const defaultBlock = createBlock( getDefaultBlockName() ); - const state = { - editor: { - present: { - blocks: { - byClientId: { - [ defaultBlock.clientId ]: { - ...omit( defaultBlock, 'attributes' ), - }, - }, - attributes: { - [ defaultBlock.clientId ]: { - ...defaultBlock.attributes, - modified: true, - }, - }, - order: { - '': [ defaultBlock.clientId ], - }, - }, - edits: {}, - }, - }, - initialEdits: {}, - currentPost: {}, - }; - - const content = getEditedPostContent( state ); - - expect( content ).toBe( '' ); - } ); - } ); - - describe( 'canInsertBlockType', () => { - it( 'should deny blocks that are not registered', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - }, - }, - }, - blockListSettings: {}, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/invalid' ) ).toBe( false ); - } ); - - it( 'should deny blocks that are not allowed by the editor', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - }, - }, - }, - blockListSettings: {}, - settings: { - allowedBlockTypes: [], - }, - }; - expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( false ); - } ); - - it( 'should allow blocks that are allowed by the editor', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - }, - }, - }, - blockListSettings: {}, - settings: { - allowedBlockTypes: [ 'core/test-block-a' ], - }, - }; - expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( true ); - } ); - - it( 'should deny blocks when the editor has a template lock', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - }, - }, - }, - blockListSettings: {}, - settings: { - templateLock: 'all', - }, - }; - expect( canInsertBlockType( state, 'core/test-block-a' ) ).toBe( false ); - } ); - - it( 'should deny blocks that restrict parent from being inserted into the root', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - }, - }, - }, - blockListSettings: {}, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-c' ) ).toBe( false ); - } ); - - it( 'should deny blocks that restrict parent from being inserted into a restricted parent', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - }, - }, - }, - }, - blockListSettings: {}, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( false ); - } ); - - it( 'should allow blocks that restrict parent to be inserted into an allowed parent', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-b' }, - }, - attributes: { - block1: {}, - }, - }, - }, - }, - blockListSettings: {}, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( true ); - } ); - - it( 'should deny restricted blocks from being inserted into a block that restricts allowedBlocks', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - }, - }, - }, - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-c' ], - }, - }, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-b', 'block1' ) ).toBe( false ); - } ); - - it( 'should allow allowed blocks to be inserted into a block that restricts allowedBlocks', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - }, - }, - }, - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - }, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-b', 'block1' ) ).toBe( true ); - } ); - - it( 'should prioritise parent over allowedBlocks', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-b' }, - }, - attributes: { - block1: {}, - }, - }, - }, - }, - blockListSettings: { - block1: { - allowedBlocks: [], - }, - }, - settings: {}, - }; - expect( canInsertBlockType( state, 'core/test-block-c', 'block1' ) ).toBe( true ); - } ); - } ); - - describe( 'getInserterItems', () => { - it( 'should properly list block type and reusable block items', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - }, - order: {}, - }, - edits: {}, - }, - }, - initialEdits: {}, - reusableBlocks: { - data: { - 1: { clientId: 'block1', title: 'Reusable Block 1' }, - }, - }, - currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, - }; - const items = getInserterItems( state ); - const testBlockAItem = items.find( ( item ) => item.id === 'core/test-block-a' ); - expect( testBlockAItem ).toEqual( { - id: 'core/test-block-a', - name: 'core/test-block-a', - initialAttributes: {}, - title: 'Test Block A', - icon: { - src: 'test', - }, - category: 'formatting', - keywords: [ 'testing' ], - isDisabled: false, - utility: 0, - frecency: 0, - hasChildBlocksWithInserterSupport: false, - } ); - const reusableBlockItem = items.find( ( item ) => item.id === 'core/block/1' ); - expect( reusableBlockItem ).toEqual( { - id: 'core/block/1', - name: 'core/block', - initialAttributes: { ref: 1 }, - title: 'Reusable Block 1', - icon: { - src: 'test', - }, - category: 'reusable', - keywords: [], - isDisabled: false, - utility: 0, - frecency: 0, - } ); + + expect( getSuggestedPostFormat( state ) ).toBe( 'image' ); } ); - it( 'should not list a reusable block item if it is being inserted inside it self', () => { + it( 'returns Quote if the first block is of type `core/quote`', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - block1ref: { - name: 'core/block', - clientId: 'block1ref', - }, - itselfBlock1: { name: 'core/test-block-a' }, - itselfBlock2: { name: 'core/test-block-b' }, - }, - attributes: { - block1ref: { - attributes: { - ref: 1, - }, - }, - itselfBlock1: {}, - itselfBlock2: {}, - }, - order: { - '': [ 'block1ref' ], + blocks: { + value: [ + { + clientId: 456, + name: 'core/quote', + attributes: {}, }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: { - 1: { clientId: 'itselfBlock1', title: 'Reusable Block 1' }, - 2: { clientId: 'itselfBlock2', title: 'Reusable Block 2' }, - }, - }, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, - }; - const items = getInserterItems( state, 'itselfBlock1' ); - const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); - expect( reusableBlockItems ).toHaveLength( 1 ); - expect( reusableBlockItems[ 0 ] ).toEqual( { - id: 'core/block/2', - name: 'core/block', - initialAttributes: { ref: 2 }, - title: 'Reusable Block 2', - icon: { - src: 'test', - }, - category: 'reusable', - keywords: [], - isDisabled: false, - utility: 0, - frecency: 0, - } ); + }; + + expect( getSuggestedPostFormat( state ) ).toBe( 'quote' ); } ); - it( 'should not list a reusable block item if it is being inserted inside a descendent', () => { + it( 'returns Video if the first block is of type `core-embed/youtube`', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - block2ref: { - name: 'core/block', - clientId: 'block1ref', - }, - referredBlock1: { name: 'core/test-block-a' }, - referredBlock2: { name: 'core/test-block-b' }, - childReferredBlock2: { name: 'core/test-block-a' }, - grandchildReferredBlock2: { name: 'core/test-block-b' }, - }, - attributes: { - block2ref: { - attributes: { - ref: 2, - }, - }, - referredBlock1: {}, - referredBlock2: {}, - childReferredBlock2: {}, - grandchildReferredBlock2: {}, - }, - order: { - '': [ 'block2ref' ], - referredBlock2: [ 'childReferredBlock2' ], - childReferredBlock2: [ 'grandchildReferredBlock2' ], + blocks: { + value: [ + { + clientId: 567, + name: 'core-embed/youtube', + attributes: {}, }, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: { - 1: { clientId: 'referredBlock1', title: 'Reusable Block 1' }, - 2: { clientId: 'referredBlock2', title: 'Reusable Block 2' }, - }, - }, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, - }; - const items = getInserterItems( state, 'grandchildReferredBlock2' ); - const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); - expect( reusableBlockItems ).toHaveLength( 1 ); - expect( reusableBlockItems[ 0 ] ).toEqual( { - id: 'core/block/1', - name: 'core/block', - initialAttributes: { ref: 1 }, - title: 'Reusable Block 1', - icon: { - src: 'test', - }, - category: 'reusable', - keywords: [], - isDisabled: false, - utility: 0, - frecency: 0, - } ); + }; + + expect( getSuggestedPostFormat( state ) ).toBe( 'video' ); } ); - it( 'should order items by descending utility and frecency', () => { + + it( 'returns Quote if the first block is of type `core/quote` and second is of type `core/paragraph`', () => { const state = { editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, + blocks: { + value: [ + { + clientId: 456, + name: 'core/quote', + attributes: {}, }, - attributes: { - block1: {}, - block2: {}, + { + clientId: 789, + name: 'core/paragraph', + attributes: {}, }, - order: {}, - }, - edits: {}, + ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: { - 1: { clientId: 'block1', title: 'Reusable Block 1' }, - 2: { clientId: 'block1', title: 'Reusable Block 2' }, - }, - }, currentPost: {}, - preferences: { - insertUsage: { - 'core/block/1': { count: 10, time: 1000 }, - 'core/block/2': { count: 20, time: 1000 }, + }; + + expect( getSuggestedPostFormat( state ) ).toBe( 'quote' ); + } ); + } ); + + describe( 'getEditedPostContent', () => { + let originalDefaultBlockName; + + beforeAll( () => { + originalDefaultBlockName = getDefaultBlockName(); + + registerBlockType( 'core/default', { + category: 'common', + title: 'default', + attributes: { + modified: { + type: 'boolean', + default: false, }, }, - blockListSettings: {}, - settings: {}, - }; - const itemIDs = getInserterItems( state ).map( ( item ) => item.id ); - expect( itemIDs ).toEqual( [ - 'core/block/2', - 'core/block/1', - 'core/test-block-b', - 'core/test-freeform', - 'core/test-default', - 'core/test-block-a', - ] ); + save: () => null, + } ); + setDefaultBlockName( 'core/default' ); + } ); + + afterAll( () => { + setDefaultBlockName( originalDefaultBlockName ); + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); } ); - it( 'should correctly cache the return values', () => { + it( 'defers to returning an edited post attribute', () => { + const block = createBlock( 'core/block' ); + const state = { editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, - block3: { name: 'core/test-block-a' }, - block4: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - block2: {}, - block3: {}, - block4: {}, - }, - order: { - '': [ 'block3', 'block4' ], - }, - }, - edits: {}, + blocks: { + value: [ block ], }, - }, - initialEdits: {}, - reusableBlocks: { - data: { - 1: { clientId: 'block1', title: 'Reusable Block 1' }, - 2: { clientId: 'block1', title: 'Reusable Block 2' }, + edits: { + content: 'custom edit', }, }, + initialEdits: {}, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, }; - const stateSecondBlockRestricted = { - ...state, - blockListSettings: { - block4: { - allowedBlocks: [ 'core/test-block-b' ], + const content = getEditedPostContent( state ); + + expect( content ).toBe( 'custom edit' ); + } ); + + it( 'returns serialization of blocks', () => { + const block = createBlock( 'core/block' ); + + const state = { + editor: { + blocks: { + value: [ block ], }, + edits: {}, }, + initialEdits: {}, + currentPost: {}, }; - const firstBlockFirstCall = getInserterItems( state, 'block3' ); - const firstBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block3' ); - expect( firstBlockFirstCall ).toBe( firstBlockSecondCall ); - expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ - 'core/test-block-b', - 'core/test-freeform', - 'core/test-default', - 'core/test-block-a', - 'core/block/1', - 'core/block/2', - ] ); + const content = getEditedPostContent( state ); - const secondBlockFirstCall = getInserterItems( state, 'block4' ); - const secondBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block4' ); - expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ - 'core/test-block-b', - 'core/test-freeform', - 'core/test-default', - 'core/test-block-a', - 'core/block/1', - 'core/block/2', - ] ); - expect( secondBlockSecondCall.map( ( item ) => item.id ) ).toEqual( [ - 'core/test-block-b', - ] ); + expect( content ).toBe( '' ); } ); - it( 'should set isDisabled when a block with `multiple: false` has been used', () => { + it( 'returns removep\'d serialization of blocks for single unknown', () => { + const unknownBlock = createBlock( 'core/test-freeform', { + content: '

foo

', + } ); const state = { editor: { - present: { - blocks: { - byClientId: { - block1: { clientId: 'block1', name: 'core/test-block-b' }, - }, - attributes: { - block1: { attribute: {} }, - }, - order: { - '': [ 'block1' ], - }, - }, - edits: {}, + blocks: { + value: [ unknownBlock ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: {}, - }, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, }; - const items = getInserterItems( state ); - const testBlockBItem = items.find( ( item ) => item.id === 'core/test-block-b' ); - expect( testBlockBItem.isDisabled ).toBe( true ); + + const content = getEditedPostContent( state ); + + expect( content ).toBe( 'foo' ); } ); - it( 'should give common blocks a low utility', () => { + it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => { + const firstUnknown = createBlock( 'core/test-freeform', { + content: '

foo

', + } ); + const secondUnknown = createBlock( 'core/test-freeform', { + content: '

bar

', + } ); const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [ firstUnknown, secondUnknown ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: {}, - }, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, }; - const items = getInserterItems( state ); - const testBlockBItem = items.find( ( item ) => item.id === 'core/test-block-b' ); - expect( testBlockBItem.utility ).toBe( INSERTER_UTILITY_LOW ); + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '

foo

\n\n

bar

' ); } ); - it( 'should give used blocks a medium utility and set a frecency', () => { + it( 'returns empty string for single unmodified default block', () => { + const defaultBlock = createBlock( getDefaultBlockName() ); const state = { editor: { - present: { - blocks: { - byClientId: {}, - attributes: {}, - order: {}, - }, - edits: {}, + blocks: { + value: [ defaultBlock ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: {}, - }, currentPost: {}, - preferences: { - insertUsage: { - 'core/test-block-b': { count: 10, time: 1000 }, - }, - }, - blockListSettings: {}, - settings: {}, }; - const items = getInserterItems( state ); - const reusableBlock2Item = items.find( ( item ) => item.id === 'core/test-block-b' ); - expect( reusableBlock2Item.utility ).toBe( INSERTER_UTILITY_MEDIUM ); - expect( reusableBlock2Item.frecency ).toBe( 2.5 ); + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '' ); } ); - it( 'should give contextual blocks a high utility', () => { + it( 'should not return empty string for modified default block', () => { + const defaultBlock = createBlock( getDefaultBlockName() ); const state = { editor: { - present: { - blocks: { - byClientId: { - block1: { name: 'core/test-block-b' }, - }, + blocks: { + value: [ { + ...defaultBlock, attributes: { - block1: { attribute: {} }, + ...defaultBlock.attributes, + modified: true, }, - order: { - '': [ 'block1' ], - }, - }, - edits: {}, + } ], }, + edits: {}, }, initialEdits: {}, - reusableBlocks: { - data: {}, - }, currentPost: {}, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - settings: {}, }; - const items = getInserterItems( state, 'block1' ); - const testBlockCItem = items.find( ( item ) => item.id === 'core/test-block-c' ); - expect( testBlockCItem.utility ).toBe( INSERTER_UTILITY_HIGH ); + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '' ); } ); } ); @@ -5289,92 +2338,12 @@ describe( 'selectors', () => { } ); } ); - describe( 'isValidTemplate', () => { - it( 'should return true if template is valid', () => { - const state = { - template: { isValid: true }, - }; - - expect( isValidTemplate( state ) ).toBe( true ); - } ); - - it( 'should return false if template is not valid', () => { - const state = { - template: { isValid: false }, - }; - - expect( isValidTemplate( state ) ).toBe( false ); - } ); - } ); - - describe( 'getTemplate', () => { - it( 'should return the template object', () => { - const template = []; - const state = { - settings: { template }, - }; - - expect( getTemplate( state ) ).toBe( template ); - } ); - } ); - - describe( 'getTemplateLock', () => { - it( 'should return the general template lock if no clientId was set', () => { - const state = { - settings: { templateLock: 'all' }, - }; - - expect( getTemplateLock( state ) ).toBe( 'all' ); - } ); - - it( 'should return null if the specified clientId was not found ', () => { - const state = { - settings: { templateLock: 'all' }, - blockListSettings: { - chicken: { - templateLock: 'insert', - }, - }, - }; - - expect( getTemplateLock( state, 'ribs' ) ).toBe( null ); - } ); - - it( 'should return null if template lock was not set on the specified block', () => { - const state = { - settings: { templateLock: 'all' }, - blockListSettings: { - chicken: { - test: 'tes1t', - }, - }, - }; - - expect( getTemplateLock( state, 'ribs' ) ).toBe( null ); - } ); - - it( 'should return the template lock for the specified clientId', () => { - const state = { - settings: { templateLock: 'all' }, - blockListSettings: { - chicken: { - templateLock: 'insert', - }, - }, - }; - - expect( getTemplateLock( state, 'chicken' ) ).toBe( 'insert' ); - } ); - } ); - describe( 'isPermalinkEditable', () => { it( 'should be false if there is no permalink', () => { const state = { currentPost: { permalink_template: '' }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5386,9 +2355,7 @@ describe( 'selectors', () => { const state = { currentPost: { permalink_template: 'http://foo.test/bar/%baz%/' }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5400,9 +2367,7 @@ describe( 'selectors', () => { const state = { currentPost: { permalink_template: 'http://foo.test/bar/%postname%/' }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5414,9 +2379,7 @@ describe( 'selectors', () => { const state = { currentPost: { permalink_template: 'http://foo.test/bar/%pagename%/' }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5431,9 +2394,7 @@ describe( 'selectors', () => { const state = { currentPost: { permalink_template: url }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5448,9 +2409,7 @@ describe( 'selectors', () => { slug: 'baz', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5461,9 +2420,7 @@ describe( 'selectors', () => { it( 'should return null if the post has no permalink template', () => { const state = { currentPost: {}, - editor: { - present: {}, - }, + editor: {}, }; expect( getPermalink( state ) ).toBeNull(); @@ -5483,9 +2440,7 @@ describe( 'selectors', () => { slug: 'baz', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5504,9 +2459,7 @@ describe( 'selectors', () => { slug: 'baz', }, editor: { - present: { - edits: {}, - }, + edits: {}, }, initialEdits: {}, }; @@ -5517,42 +2470,13 @@ describe( 'selectors', () => { it( 'should return null if the post has no permalink template', () => { const state = { currentPost: {}, - editor: { - present: {}, - }, + editor: {}, }; expect( getPermalinkParts( state ) ).toBeNull(); } ); } ); - describe( 'getBlockListSettings', () => { - it( 'should return the settings of a block', () => { - const state = { - blockListSettings: { - chicken: { - setting1: false, - }, - ribs: { - setting2: true, - }, - }, - }; - - expect( getBlockListSettings( state, 'chicken' ) ).toEqual( { - setting1: false, - } ); - } ); - - it( 'should return undefined if settings for the block don’t exist', () => { - const state = { - blockListSettings: {}, - }; - - expect( getBlockListSettings( state, 'chicken' ) ).toBe( undefined ); - } ); - } ); - describe( 'canUserUseUnfilteredHTML', () => { it( 'should return true if the _links object contains the property wp:action-unfiltered-html', () => { const state = {