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 b6da0e47ef9b3..3ab9d0bd1cd3e 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -1134,10 +1134,6 @@ _Parameters_ - _firstBlockClientId_ `string`: Client ID of the first block to merge. - _secondBlockClientId_ `string`: Client ID of the second block to merge. -_Returns_ - -- `Object`: Action object. - # **moveBlocksDown** Undocumented declaration. @@ -1179,10 +1175,6 @@ _Parameters_ - _start_ `string`: First block of the multi selection. - _end_ `string`: Last block of the multiselection. -_Returns_ - -- `Object`: Action object. - # **receiveBlocks** Returns an action object used in signalling that blocks have been received. @@ -1273,10 +1265,6 @@ _Parameters_ - _blocks_ `Array`: Array of blocks. -_Returns_ - -- `Object`: Action object. - # **resetSelection** Returns an action object used in signalling that selection state should be @@ -1540,5 +1528,16 @@ _Returns_ - `Object`: Action object +# **validateBlocksToTemplate** + +Block validity is a function of blocks state (at the point of a +reset) and the template setting. As a compromise to its placement +across distinct parts of state, it is implemented here as a side- +effect of the block reset action. + +_Parameters_ + +- _blocks_ `Array`: Array of blocks. + diff --git a/package-lock.json b/package-lock.json index 8bc191f5b665d..f5fb7ffd02d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17315,6 +17315,7 @@ "@wordpress/components": "file:packages/components", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", + "@wordpress/data-controls": "file:packages/data-controls", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", @@ -17342,7 +17343,6 @@ "react-spring": "^8.0.19", "reakit": "1.1.0", "redux-multi": "^0.1.12", - "refx": "^3.0.0", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", "traverse": "^0.6.6" diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 398e79d7286df..7716626c224a2 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -35,6 +35,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/data-controls": "file:../data-controls", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", @@ -62,7 +63,6 @@ "react-spring": "^8.0.19", "reakit": "1.1.0", "redux-multi": "^0.1.12", - "refx": "^3.0.0", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", "traverse": "^0.6.6" diff --git a/packages/block-editor/src/components/provider/with-registry-provider.js b/packages/block-editor/src/components/provider/with-registry-provider.js index 60109fea59f3c..3b9f7b24c651e 100644 --- a/packages/block-editor/src/components/provider/with-registry-provider.js +++ b/packages/block-editor/src/components/provider/with-registry-provider.js @@ -13,7 +13,6 @@ import { createHigherOrderComponent } from '@wordpress/compose'; * Internal dependencies */ import { storeConfig } from '../../store'; -import applyMiddlewares from '../../store/middlewares'; const withRegistryProvider = createHigherOrderComponent( ( WrappedComponent ) => { @@ -28,12 +27,10 @@ const withRegistryProvider = createHigherOrderComponent( const [ subRegistry, setSubRegistry ] = useState( null ); useEffect( () => { const newRegistry = createRegistry( {}, registry ); - const store = newRegistry.registerStore( + newRegistry.registerStore( 'core/block-editor', storeConfig ); - // This should be removed after the refactoring of the effects to controls. - applyMiddlewares( store ); setSubRegistry( newRegistry ); }, [ registry ] ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 48eef28b2f25d..cf69468a598a2 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, first, last, some } from 'lodash'; +import { castArray, findKey, first, last, some } from 'lodash'; /** * WordPress dependencies @@ -9,12 +9,22 @@ import { castArray, first, last, some } from 'lodash'; import { cloneBlock, createBlock, + doBlocksMatchTemplate, + getBlockType, getDefaultBlockName, hasBlockSupport, + switchToBlockType, + synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; -import { __ } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { controls } from '@wordpress/data'; +import { create, insert, remove, toHTMLString } from '@wordpress/rich-text'; + +/** + * Internal dependencies + */ +import { __unstableMarkAutomaticChangeFinalControl } from '../store/controls'; /** * Generator which will yield a default block insert action if there @@ -38,14 +48,50 @@ function* ensureDefaultBlock() { * content reflected as an edit in state. * * @param {Array} blocks Array of blocks. - * - * @return {Object} Action object. */ -export function resetBlocks( blocks ) { - return { +export function* resetBlocks( blocks ) { + yield { type: 'RESET_BLOCKS', blocks, }; + return yield* validateBlocksToTemplate( blocks ); +} + +/** + * Block validity is a function of blocks state (at the point of a + * reset) and the template setting. As a compromise to its placement + * across distinct parts of state, it is implemented here as a side- + * effect of the block reset action. + * + * @param {Array} blocks Array of blocks. + */ +export function* validateBlocksToTemplate( blocks ) { + const template = yield controls.select( + 'core/block-editor', + 'getTemplate' + ); + const templateLock = yield controls.select( + 'core/block-editor', + 'getTemplateLock' + ); + + // Unlocked templates are considered always valid because they act + // as default values only. + const isBlocksValidToTemplate = + ! template || + templateLock !== 'all' || + doBlocksMatchTemplate( blocks, template ); + + // Update if validity has changed. + const isValidTemplate = yield controls.select( + 'core/block-editor', + 'isValidTemplate' + ); + + if ( isBlocksValidToTemplate !== isValidTemplate ) { + yield setTemplateValidity( isBlocksValidToTemplate ); + return isBlocksValidToTemplate; + } } /** @@ -211,15 +257,27 @@ export function stopMultiSelect() { * * @param {string} start First block of the multi selection. * @param {string} end Last block of the multiselection. - * - * @return {Object} Action object. */ -export function multiSelect( start, end ) { - return { +export function* multiSelect( start, end ) { + yield { type: 'MULTI_SELECT', start, end, }; + + const blockCount = yield controls.select( + 'core/block-editor', + 'getSelectedBlockCount' + ); + + speak( + sprintf( + /* translators: %s: number of selected blocks */ + _n( '%s block selected.', '%s blocks selected.', blockCount ), + blockCount + ), + 'assertive' + ); } /** @@ -592,10 +650,18 @@ export function setTemplateValidity( isValid ) { * * @return {Object} Action object. */ -export function synchronizeTemplate() { - return { +export function* synchronizeTemplate() { + yield { type: 'SYNCHRONIZE_TEMPLATE', }; + const blocks = yield controls.select( 'core/block-editor', 'getBlocks' ); + const template = yield controls.select( + 'core/block-editor', + 'getTemplate' + ); + const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template ); + + return yield resetBlocks( updatedBlockList ); } /** @@ -603,14 +669,165 @@ export function synchronizeTemplate() { * * @param {string} firstBlockClientId Client ID of the first block to merge. * @param {string} secondBlockClientId Client ID of the second block to merge. - * - * @return {Object} Action object. */ -export function mergeBlocks( firstBlockClientId, secondBlockClientId ) { - return { +export function* mergeBlocks( firstBlockClientId, secondBlockClientId ) { + const blocks = [ firstBlockClientId, secondBlockClientId ]; + yield { type: 'MERGE_BLOCKS', - blocks: [ firstBlockClientId, secondBlockClientId ], + blocks, }; + + const [ clientIdA, clientIdB ] = blocks; + const blockA = yield controls.select( + 'core/block-editor', + 'getBlock', + clientIdA + ); + const blockAType = getBlockType( blockA.name ); + + // Only focus the previous block if it's not mergeable + if ( ! blockAType.merge ) { + yield selectBlock( blockA.clientId ); + return; + } + + const blockB = yield controls.select( + 'core/block-editor', + 'getBlock', + clientIdB + ); + const blockBType = getBlockType( blockB.name ); + const { clientId, attributeKey, offset } = yield controls.select( + 'core/block-editor', + 'getSelectionStart' + ); + const selectedBlockType = clientId === clientIdA ? blockAType : blockBType; + const attributeDefinition = selectedBlockType.attributes[ attributeKey ]; + const canRestoreTextSelection = + ( clientId === clientIdA || clientId === clientIdB ) && + attributeKey !== undefined && + offset !== undefined && + // We cannot restore text selection if the RichText identifier + // is not a defined block attribute key. This can be the case if the + // fallback intance ID is used to store selection (and no RichText + // identifier is set), or when the identifier is wrong. + !! attributeDefinition; + + if ( ! attributeDefinition ) { + if ( typeof attributeKey === 'number' ) { + window.console.error( + `RichText needs an identifier prop that is the block attribute key of the attribute it controls. Its type is expected to be a string, but was ${ typeof attributeKey }` + ); + } else { + window.console.error( + 'The RichText identifier prop does not match any attributes defined by the block.' + ); + } + } + + // A robust way to retain selection position through various transforms + // is to insert a special character at the position and then recover it. + const START_OF_SELECTED_AREA = '\u0086'; + + // Clone the blocks so we don't insert the character in a "live" block. + const cloneA = cloneBlock( blockA ); + const cloneB = cloneBlock( blockB ); + + if ( canRestoreTextSelection ) { + const selectedBlock = clientId === clientIdA ? cloneA : cloneB; + const html = selectedBlock.attributes[ attributeKey ]; + const { + multiline: multilineTag, + __unstableMultilineWrapperTags: multilineWrapperTags, + __unstablePreserveWhiteSpace: preserveWhiteSpace, + } = attributeDefinition; + const value = insert( + create( { + html, + multilineTag, + multilineWrapperTags, + preserveWhiteSpace, + } ), + START_OF_SELECTED_AREA, + offset, + offset + ); + + selectedBlock.attributes[ attributeKey ] = toHTMLString( { + value, + multilineTag, + preserveWhiteSpace, + } ); + } + + // We can only merge blocks with similar types + // thus, we transform the block to merge first + const blocksWithTheSameType = + blockA.name === blockB.name + ? [ cloneB ] + : switchToBlockType( cloneB, blockA.name ); + + // If the block types can not match, do nothing + if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { + return; + } + + // Calling the merge to update the attributes and remove the block to be merged + const updatedAttributes = blockAType.merge( + cloneA.attributes, + blocksWithTheSameType[ 0 ].attributes + ); + + if ( canRestoreTextSelection ) { + const newAttributeKey = findKey( + updatedAttributes, + ( v ) => + typeof v === 'string' && + v.indexOf( START_OF_SELECTED_AREA ) !== -1 + ); + const convertedHtml = updatedAttributes[ newAttributeKey ]; + const { + multiline: multilineTag, + __unstableMultilineWrapperTags: multilineWrapperTags, + __unstablePreserveWhiteSpace: preserveWhiteSpace, + } = blockAType.attributes[ newAttributeKey ]; + const convertedValue = create( { + html: convertedHtml, + multilineTag, + multilineWrapperTags, + preserveWhiteSpace, + } ); + const newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA ); + const newValue = remove( convertedValue, newOffset, newOffset + 1 ); + const newHtml = toHTMLString( { + value: newValue, + multilineTag, + preserveWhiteSpace, + } ); + + updatedAttributes[ newAttributeKey ] = newHtml; + + yield selectionChange( + blockA.clientId, + newAttributeKey, + newOffset, + newOffset + ); + } + + yield* replaceBlocks( + [ blockA.clientId, blockB.clientId ], + [ + { + ...blockA, + attributes: { + ...blockA.attributes, + ...updatedAttributes, + }, + }, + ...blocksWithTheSameType.slice( 1 ), + ] + ); } /** @@ -907,11 +1124,16 @@ export function __unstableMarkNextChangeAsNotPersistent() { * after the change was made, and any actions that are a consequence of it, so * it is recommended to be called at the next idle period to ensure all * selection changes have been recorded. - * - * @return {Object} Action object. */ -export function __unstableMarkAutomaticChange() { - return { type: 'MARK_AUTOMATIC_CHANGE' }; +export function* __unstableMarkAutomaticChange() { + yield { type: 'MARK_AUTOMATIC_CHANGE' }; + yield __unstableMarkAutomaticChangeFinalControl(); +} + +export function __unstableMarkAutomaticChangeFinal() { + return { + type: 'MARK_AUTOMATIC_CHANGE_FINAL', + }; } /** diff --git a/packages/block-editor/src/store/controls.js b/packages/block-editor/src/store/controls.js index 83b4f45342546..84d97f38654bb 100644 --- a/packages/block-editor/src/store/controls.js +++ b/packages/block-editor/src/store/controls.js @@ -1,9 +1,34 @@ +/** + * WordPress dependencies + */ +import { createRegistryControl } from '@wordpress/data'; + +export const __unstableMarkAutomaticChangeFinalControl = function () { + return { + type: 'MARK_AUTOMATIC_CHANGE_FINAL_CONTROL', + }; +}; + const controls = { SLEEP( { duration } ) { return new Promise( ( resolve ) => { setTimeout( resolve, duration ); } ); }, + + MARK_AUTOMATIC_CHANGE_FINAL_CONTROL: createRegistryControl( + ( registry ) => () => { + const { + requestIdleCallback = ( callback ) => + setTimeout( callback, 100 ), + } = window; + requestIdleCallback( () => + registry + .dispatch( 'core/block-editor' ) + .__unstableMarkAutomaticChangeFinal() + ); + } + ), }; export default controls; diff --git a/packages/block-editor/src/store/effects.js b/packages/block-editor/src/store/effects.js deleted file mode 100644 index 5142020ffffa1..0000000000000 --- a/packages/block-editor/src/store/effects.js +++ /dev/null @@ -1,256 +0,0 @@ -/** - * External dependencies - */ -import { findKey } from 'lodash'; - -/** - * WordPress dependencies - */ -import { speak } from '@wordpress/a11y'; -import { - getBlockType, - doBlocksMatchTemplate, - switchToBlockType, - synchronizeBlocksWithTemplate, - cloneBlock, -} from '@wordpress/blocks'; -import { _n, sprintf } from '@wordpress/i18n'; -import { create, toHTMLString, insert, remove } from '@wordpress/rich-text'; - -/** - * Internal dependencies - */ -import { - replaceBlocks, - selectBlock, - setTemplateValidity, - resetBlocks, - selectionChange, -} from './actions'; -import { - getBlock, - getBlocks, - getSelectedBlockCount, - getTemplateLock, - getTemplate, - isValidTemplate, - getSelectionStart, -} from './selectors'; - -/** - * Block validity is a function of blocks state (at the point of a - * reset) and the template setting. As a compromise to its placement - * across distinct parts of state, it is implemented here as a side- - * effect of the block reset action. - * - * @param {Object} action RESET_BLOCKS action. - * @param {Object} store Store instance. - * - * @return {?Object} New validity set action if validity has changed. - */ -export function validateBlocksToTemplate( action, store ) { - const state = store.getState(); - const template = getTemplate( state ); - const templateLock = getTemplateLock( state ); - - // Unlocked templates are considered always valid because they act - // as default values only. - const isBlocksValidToTemplate = - ! template || - templateLock !== 'all' || - doBlocksMatchTemplate( action.blocks, template ); - - // Update if validity has changed. - if ( isBlocksValidToTemplate !== isValidTemplate( state ) ) { - return setTemplateValidity( isBlocksValidToTemplate ); - } -} - -export default { - MERGE_BLOCKS( action, store ) { - const { dispatch } = store; - const state = store.getState(); - const [ clientIdA, clientIdB ] = action.blocks; - const blockA = getBlock( state, clientIdA ); - const blockAType = getBlockType( blockA.name ); - - // Only focus the previous block if it's not mergeable - if ( ! blockAType.merge ) { - dispatch( selectBlock( blockA.clientId ) ); - return; - } - - const blockB = getBlock( state, clientIdB ); - const blockBType = getBlockType( blockB.name ); - const { clientId, attributeKey, offset } = getSelectionStart( state ); - const selectedBlockType = - clientId === clientIdA ? blockAType : blockBType; - const attributeDefinition = - selectedBlockType.attributes[ attributeKey ]; - const canRestoreTextSelection = - ( clientId === clientIdA || clientId === clientIdB ) && - attributeKey !== undefined && - offset !== undefined && - // We cannot restore text selection if the RichText identifier - // is not a defined block attribute key. This can be the case if the - // fallback intance ID is used to store selection (and no RichText - // identifier is set), or when the identifier is wrong. - !! attributeDefinition; - - if ( ! attributeDefinition ) { - if ( typeof attributeKey === 'number' ) { - window.console.error( - `RichText needs an identifier prop that is the block attribute key of the attribute it controls. Its type is expected to be a string, but was ${ typeof attributeKey }` - ); - } else { - window.console.error( - 'The RichText identifier prop does not match any attributes defined by the block.' - ); - } - } - - // A robust way to retain selection position through various transforms - // is to insert a special character at the position and then recover it. - const START_OF_SELECTED_AREA = '\u0086'; - - // Clone the blocks so we don't insert the character in a "live" block. - const cloneA = cloneBlock( blockA ); - const cloneB = cloneBlock( blockB ); - - if ( canRestoreTextSelection ) { - const selectedBlock = clientId === clientIdA ? cloneA : cloneB; - const html = selectedBlock.attributes[ attributeKey ]; - const { - multiline: multilineTag, - __unstableMultilineWrapperTags: multilineWrapperTags, - __unstablePreserveWhiteSpace: preserveWhiteSpace, - } = attributeDefinition; - const value = insert( - create( { - html, - multilineTag, - multilineWrapperTags, - preserveWhiteSpace, - } ), - START_OF_SELECTED_AREA, - offset, - offset - ); - - selectedBlock.attributes[ attributeKey ] = toHTMLString( { - value, - multilineTag, - preserveWhiteSpace, - } ); - } - - // We can only merge blocks with similar types - // thus, we transform the block to merge first - const blocksWithTheSameType = - blockA.name === blockB.name - ? [ cloneB ] - : switchToBlockType( cloneB, blockA.name ); - - // If the block types can not match, do nothing - if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { - return; - } - - // Calling the merge to update the attributes and remove the block to be merged - const updatedAttributes = blockAType.merge( - cloneA.attributes, - blocksWithTheSameType[ 0 ].attributes - ); - - if ( canRestoreTextSelection ) { - const newAttributeKey = findKey( - updatedAttributes, - ( v ) => - typeof v === 'string' && - v.indexOf( START_OF_SELECTED_AREA ) !== -1 - ); - const convertedHtml = updatedAttributes[ newAttributeKey ]; - const { - multiline: multilineTag, - __unstableMultilineWrapperTags: multilineWrapperTags, - __unstablePreserveWhiteSpace: preserveWhiteSpace, - } = blockAType.attributes[ newAttributeKey ]; - const convertedValue = create( { - html: convertedHtml, - multilineTag, - multilineWrapperTags, - preserveWhiteSpace, - } ); - const newOffset = convertedValue.text.indexOf( - START_OF_SELECTED_AREA - ); - const newValue = remove( convertedValue, newOffset, newOffset + 1 ); - const newHtml = toHTMLString( { - value: newValue, - multilineTag, - preserveWhiteSpace, - } ); - - updatedAttributes[ newAttributeKey ] = newHtml; - - dispatch( - selectionChange( - blockA.clientId, - newAttributeKey, - newOffset, - newOffset - ) - ); - } - - dispatch( - replaceBlocks( - [ blockA.clientId, blockB.clientId ], - [ - { - ...blockA, - attributes: { - ...blockA.attributes, - ...updatedAttributes, - }, - }, - ...blocksWithTheSameType.slice( 1 ), - ] - ) - ); - }, - RESET_BLOCKS: [ validateBlocksToTemplate ], - MULTI_SELECT: ( action, { getState } ) => { - const blockCount = getSelectedBlockCount( getState() ); - - speak( - sprintf( - /* translators: %s: number of selected blocks */ - _n( '%s block selected.', '%s blocks selected.', blockCount ), - blockCount - ), - 'assertive' - ); - }, - SYNCHRONIZE_TEMPLATE( action, { getState } ) { - const state = getState(); - const blocks = getBlocks( state ); - const template = getTemplate( state ); - const updatedBlockList = synchronizeBlocksWithTemplate( - blocks, - template - ); - - return resetBlocks( updatedBlockList ); - }, - MARK_AUTOMATIC_CHANGE( action, store ) { - const { - setTimeout, - requestIdleCallback = ( callback ) => setTimeout( callback, 100 ), - } = window; - - requestIdleCallback( () => { - store.dispatch( { type: 'MARK_AUTOMATIC_CHANGE_FINAL' } ); - } ); - }, -}; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 8fd1f3532c632..4ba6668916677 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -7,7 +7,6 @@ import { createReduxStore, registerStore } from '@wordpress/data'; * Internal dependencies */ import reducer from './reducer'; -import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; @@ -44,10 +43,7 @@ export const store = createReduxStore( STORE_NAME, { } ); // Ideally we'd use register instead of register stores. -// We should be able to make the switch once we remove the "effects" middleware. -// We also need a more generic way of defining persistence and not rely on a plugin. -const instantiatedStore = registerStore( STORE_NAME, { +registerStore( STORE_NAME, { ...storeConfig, persist: [ 'preferences' ], } ); -applyMiddlewares( instantiatedStore ); diff --git a/packages/block-editor/src/store/middlewares.js b/packages/block-editor/src/store/middlewares.js deleted file mode 100644 index 0f4c5aef8df70..0000000000000 --- a/packages/block-editor/src/store/middlewares.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import refx from 'refx'; -import multi from 'redux-multi'; -import { flowRight } from 'lodash'; - -/** - * Internal dependencies - */ -import effects from './effects'; - -/** - * Applies the custom middlewares used specifically in the editor module. - * - * @param {Object} store Store Object. - * - * @return {Object} Update Store Object. - */ -function applyMiddlewares( store ) { - const middlewares = [ refx( effects ), multi ]; - - let enhancedDispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ); - }; - let chain = []; - - const middlewareAPI = { - getState: store.getState, - dispatch: ( ...args ) => enhancedDispatch( ...args ), - }; - chain = middlewares.map( ( middleware ) => middleware( middlewareAPI ) ); - enhancedDispatch = flowRight( ...chain )( store.dispatch ); - - store.dispatch = enhancedDispatch; - return store; -} - -export default applyMiddlewares; diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index aa5f622598700..d9598b9a7e6b8 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -41,10 +41,10 @@ import { describe( 'actions', () => { describe( 'resetBlocks', () => { - it( 'should return the RESET_BLOCKS actions', () => { + it( 'should yield the RESET_BLOCKS actions', () => { const blocks = []; - const result = resetBlocks( blocks ); - expect( result ).toEqual( { + const fulfillment = resetBlocks( blocks ); + expect( fulfillment.next().value ).toEqual( { type: 'RESET_BLOCKS', blocks, } ); @@ -119,7 +119,8 @@ describe( 'actions', () => { it( 'should return MULTI_SELECT action', () => { const start = 'start'; const end = 'end'; - expect( multiSelect( start, end ) ).toEqual( { + const fulfillment = multiSelect( start, end ); + expect( fulfillment.next().value ).toEqual( { type: 'MULTI_SELECT', start, end, @@ -740,9 +741,11 @@ describe( 'actions', () => { it( 'should return MERGE_BLOCKS action', () => { const firstBlockClientId = 'blockA'; const secondBlockClientId = 'blockB'; - expect( - mergeBlocks( firstBlockClientId, secondBlockClientId ) - ).toEqual( { + const fulfillment = mergeBlocks( + firstBlockClientId, + secondBlockClientId + ); + expect( fulfillment.next().value ).toEqual( { type: 'MERGE_BLOCKS', blocks: [ firstBlockClientId, secondBlockClientId ], } ); diff --git a/packages/block-editor/src/store/test/effects.js b/packages/block-editor/src/store/test/effects.js index 1832e7fe2a829..9a8ecc4980187 100644 --- a/packages/block-editor/src/store/test/effects.js +++ b/packages/block-editor/src/store/test/effects.js @@ -21,16 +21,13 @@ import { createRegistry } from '@wordpress/data'; import actions, { updateSettings, mergeBlocks, - replaceBlocks, resetBlocks, selectBlock, selectionChange, - setTemplateValidity, + validateBlocksToTemplate, } from '../actions'; -import effects, { validateBlocksToTemplate } from '../effects'; import * as selectors from '../selectors'; import reducer from '../reducer'; -import applyMiddlewares from '../middlewares'; import '../../'; describe( 'effects', () => { @@ -44,14 +41,10 @@ describe( 'effects', () => { }; 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', () => { @@ -64,19 +57,21 @@ describe( 'effects', () => { 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, + const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId ); + expect( fulfillment.next() ).toEqual( { + done: false, + value: { + type: 'MERGE_BLOCKS', + blocks: [ blockA.clientId, blockB.clientId ], + }, } ); - - expect( dispatch ).toHaveBeenCalledTimes( 1 ); - expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken' ) ); + fulfillment.next(); + expect( fulfillment.next( blockA ) ).toEqual( { + done: false, + value: selectBlock( 'chicken' ), + } ); + expect( fulfillment.next( blockA ).done ).toEqual( true ); } ); it( 'should merge the blocks if blocks of the same type', () => { @@ -108,24 +103,23 @@ describe( 'effects', () => { attributes: { content: 'ribs' }, innerBlocks: [], } ); - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( { - selectionStart: { - clientId: blockB.clientId, - attributeKey: 'content', - offset: 0, - }, - } ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { - dispatch, - getState, - } ); - expect( dispatch ).toHaveBeenCalledTimes( 2 ); - expect( dispatch ).toHaveBeenCalledWith( + const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId ); + // MERGE_BLOCKS + fulfillment.next(); + // getBlock A + fulfillment.next(); + fulfillment.next( blockA ); + // getBlock B + fulfillment.next( blockB ); + // getSelectionStart + fulfillment.next( { + clientId: blockB.clientId, + attributeKey: 'content', + offset: 0, + } ); + // selectionChange + fulfillment.next( selectionChange( blockA.clientId, 'content', @@ -133,22 +127,19 @@ describe( 'effects', () => { 'chicken'.length + 1 ) ); - const lastCall = dispatch.mock.calls[ 1 ]; - expect( lastCall ).toHaveLength( 1 ); - const [ lastCallArgument ] = lastCall; - const expectedGenerator = replaceBlocks( - [ 'chicken', 'ribs' ], - [ + fulfillment.next(); + fulfillment.next(); + expect( fulfillment.next( blockA ).value ).toMatchObject( { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken', 'ribs' ], + blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: { content: 'chicken ribs' }, }, - ] - ); - expect( Array.from( lastCallArgument ) ).toEqual( - Array.from( expectedGenerator ) - ); + ], + } ); } ); it( 'should not merge the blocks have different types without transformation', () => { @@ -181,23 +172,28 @@ describe( 'effects', () => { attributes: { content: 'ribs' }, innerBlocks: [], } ); - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( { - selectionStart: { - clientId: blockB.clientId, - attributeKey: 'content', - offset: 0, - }, + + const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId ); + // MERGE_BLOCKS + fulfillment.next(); + // getBlock A + fulfillment.next(); + fulfillment.next( blockA ); + // getBlock B + expect( fulfillment.next( blockB ).value ).toEqual( { + args: [], + selectorName: 'getSelectionStart', + storeKey: 'core/block-editor', + type: '@@data/SELECT', } ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { - dispatch, - getState, + // getSelectionStart + const next = fulfillment.next( { + clientId: blockB.clientId, + attributeKey: 'content', + offset: 0, } ); - - expect( dispatch ).not.toHaveBeenCalled(); + expect( next.value ).toEqual( undefined ); + expect( next.done ).toBe( true ); } ); it( 'should transform and merge the blocks', () => { @@ -254,24 +250,27 @@ describe( 'effects', () => { attributes: { content2: 'ribs' }, innerBlocks: [], } ); - selectors.getBlock = ( state, clientId ) => { - return blockA.clientId === clientId ? blockA : blockB; - }; - const dispatch = jest.fn(); - const getState = () => ( { - selectionStart: { + + const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId ); + // MERGE_BLOCKS + fulfillment.next(); + // getBlock A + fulfillment.next(); + fulfillment.next( blockA ); + // getBlock B + expect( fulfillment.next( blockB ).value ).toEqual( { + args: [], + selectorName: 'getSelectionStart', + storeKey: 'core/block-editor', + type: '@@data/SELECT', + } ); + expect( + fulfillment.next( { clientId: blockB.clientId, attributeKey: 'content2', offset: 0, - }, - } ); - handler( mergeBlocks( blockA.clientId, blockB.clientId ), { - dispatch, - getState, - } ); - - expect( dispatch ).toHaveBeenCalledTimes( 2 ); - expect( dispatch ).toHaveBeenCalledWith( + } ).value + ).toEqual( selectionChange( blockA.clientId, 'content', @@ -279,34 +278,32 @@ describe( 'effects', () => { 'chicken'.length + 1 ) ); - const expectedGenerator = replaceBlocks( - [ 'chicken', 'ribs' ], - [ + + fulfillment.next(); + fulfillment.next(); + fulfillment.next(); + expect( fulfillment.next( blockA ).value ).toMatchObject( { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken', 'ribs' ], + blocks: [ { clientId: 'chicken', name: 'core/test-block', attributes: { content: 'chicken ribs' }, }, - ] - ); - const lastCall = dispatch.mock.calls[ 1 ]; - expect( lastCall ).toHaveLength( 1 ); - const [ lastCallArgument ] = lastCall; - expect( Array.from( lastCallArgument ) ).toEqual( - Array.from( expectedGenerator ) - ); + ], + } ); } ); } ); describe( 'validateBlocksToTemplate', () => { let store; beforeEach( () => { - store = createRegistry().registerStore( 'test', { + store = createRegistry().registerStore( 'core/block-editor', { actions, selectors, reducer, } ); - applyMiddlewares( store ); registerBlockType( 'core/test-block', defaultBlockSettings ); } ); @@ -317,31 +314,32 @@ describe( 'effects', () => { } ); } ); - it( 'should return undefined if no template assigned', () => { - const result = validateBlocksToTemplate( - resetBlocks( [ createBlock( 'core/test-block' ) ] ), - store + it( 'should return undefined if no template assigned', async () => { + const result = await store.dispatch( + validateBlocksToTemplate( + resetBlocks( [ createBlock( 'core/test-block' ) ] ), + store + ) ); - expect( result ).toBe( undefined ); + expect( result ).toEqual( undefined ); } ); - it( 'should return undefined if invalid but unlocked', () => { + it( 'should return undefined if invalid but unlocked', async () => { store.dispatch( updateSettings( { template: [ [ 'core/foo', {} ] ], } ) ); - const result = validateBlocksToTemplate( - resetBlocks( [ createBlock( 'core/test-block' ) ] ), - store + const result = await store.dispatch( + validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] ) ); - expect( result ).toBe( undefined ); + expect( result ).toEqual( undefined ); } ); - it( 'should return undefined if locked and valid', () => { + it( 'should return undefined if locked and valid', async () => { store.dispatch( updateSettings( { template: [ [ 'core/test-block' ] ], @@ -349,15 +347,14 @@ describe( 'effects', () => { } ) ); - const result = validateBlocksToTemplate( - resetBlocks( [ createBlock( 'core/test-block' ) ] ), - store + const result = await store.dispatch( + validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] ) ); - expect( result ).toBe( undefined ); + expect( result ).toEqual( undefined ); } ); - it( 'should return validity set action if invalid on default state', () => { + it( 'should return validity set action if invalid on default state', async () => { store.dispatch( updateSettings( { template: [ [ 'core/foo' ] ], @@ -365,12 +362,11 @@ describe( 'effects', () => { } ) ); - const result = validateBlocksToTemplate( - resetBlocks( [ createBlock( 'core/test-block' ) ] ), - store + const result = await store.dispatch( + validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] ) ); - expect( result ).toEqual( setTemplateValidity( false ) ); + expect( result ).toEqual( false ); } ); } ); } );