From 7c31ce3267ac944faeec53332ec0832ee88d7e41 Mon Sep 17 00:00:00 2001 From: Noah Allen Date: Mon, 11 May 2020 19:02:25 -0700 Subject: [PATCH] Add unit tests for useBlockSync hook --- .../provider/test/use-block-sync.js | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 packages/block-editor/src/components/provider/test/use-block-sync.js diff --git a/packages/block-editor/src/components/provider/test/use-block-sync.js b/packages/block-editor/src/components/provider/test/use-block-sync.js new file mode 100644 index 00000000000000..29b6b15ed3f1d0 --- /dev/null +++ b/packages/block-editor/src/components/provider/test/use-block-sync.js @@ -0,0 +1,368 @@ +/** + * External dependencies + */ +import { create, act } from 'react-test-renderer'; + +/** + * Internal dependencies + */ +import useBlockSync from '../use-block-sync'; +import withRegistryProvider from '../with-registry-provider'; +import * as blockEditorActions from '../../../store/actions'; + +const TestWrapper = withRegistryProvider( ( props ) => { + if ( props.setRegistry ) { + props.setRegistry( props.registry ); + } + useBlockSync( props ); + return

Test.

; +} ); + +describe( 'useBlockSync hook', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + it( 'resets the block-editor blocks when the controll value changes', async () => { + const fakeBlocks = []; + const resetBlocks = jest.spyOn( blockEditorActions, 'resetBlocks' ); + const replaceInnerBlocks = jest.spyOn( + blockEditorActions, + 'replaceInnerBlocks' + ); + const onChange = jest.fn(); + const onInput = jest.fn(); + + let root; + await act( async () => { + root = create( + + ); + } ); + + // Reset blocks should be called on mount. + expect( onChange ).not.toHaveBeenCalled(); + expect( onInput ).not.toHaveBeenCalled(); + expect( replaceInnerBlocks ).not.toHaveBeenCalled(); + expect( resetBlocks ).toHaveBeenCalledWith( fakeBlocks ); + + const testBlocks = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + await act( async () => { + root.update( + + ); + } ); + + // Reset blocks should be called when the incoming value changes. + expect( onChange ).not.toHaveBeenCalled(); + expect( onInput ).not.toHaveBeenCalled(); + expect( replaceInnerBlocks ).not.toHaveBeenCalled(); + expect( resetBlocks ).toHaveBeenCalledWith( testBlocks ); + } ); + + it( 'replaces the inner blocks of a block when the control value changes if a clientId is passed', async () => { + const fakeBlocks = []; + const replaceInnerBlocks = jest.spyOn( + blockEditorActions, + 'replaceInnerBlocks' + ); + const resetBlocks = jest.spyOn( blockEditorActions, 'resetBlocks' ); + const onChange = jest.fn(); + const onInput = jest.fn(); + + let root; + await act( async () => { + root = create( + + ); + } ); + + expect( resetBlocks ).not.toHaveBeenCalled(); + expect( onChange ).not.toHaveBeenCalled(); + expect( onInput ).not.toHaveBeenCalled(); + expect( replaceInnerBlocks ).toHaveBeenCalledWith( + 'test', // It should use the given client ID. + fakeBlocks, // It should use the controlled blocks value. + false // It shoudl not update the selection state. + ); + + const testBlocks = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + await act( async () => { + root.update( + + ); + } ); + + // Reset blocks should be called when the incoming value changes. + expect( onChange ).not.toHaveBeenCalled(); + expect( onInput ).not.toHaveBeenCalled(); + expect( resetBlocks ).not.toHaveBeenCalled(); + expect( replaceInnerBlocks ).toHaveBeenCalledWith( + 'test', + testBlocks, + false + ); + } ); + + it( 'does not add the controlled blocks to the block-editor store if the store already contains them', async () => { + const replaceInnerBlocks = jest.spyOn( + blockEditorActions, + 'replaceInnerBlocks' + ); + const onChange = jest.fn(); + const onInput = jest.fn(); + + const value1 = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + let root; + let registry; + const setRegistry = ( reg ) => { + registry = reg; + }; + await act( async () => { + root = create( + + ); + } ); + + registry + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( 'a', { foo: 2 } ); + + const newBlockValue = registry + .select( 'core/block-editor' ) + .getBlocks( 'test' ); + replaceInnerBlocks.mockClear(); + + // Assert that the reference has changed so that the side effect will be + // triggered once more. + expect( newBlockValue ).not.toBe( value1 ); + + await act( async () => { + root.update( + + ); + } ); + + // replaceInnerBlocks should not be called when the controlling + // block value is the same as what already exists in the store. + expect( replaceInnerBlocks ).not.toHaveBeenCalled(); + } ); + + it( 'sets a block as an inner block controller if a clientId is provided', async () => { + const setAsController = jest.spyOn( + blockEditorActions, + 'setHasControlledInnerBlocks' + ); + + await act( async () => { + create( + + ); + } ); + expect( setAsController ).toHaveBeenCalledWith( 'test', true ); + } ); + + it( 'calls onInput when a non-persistent block change occurs', async () => { + const onChange = jest.fn(); + const onInput = jest.fn(); + + const value1 = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + let registry; + const setRegistry = ( reg ) => { + registry = reg; + }; + await act( async () => { + create( + + ); + } ); + onChange.mockClear(); + onInput.mockClear(); + + // Create a non-persistent change. + registry + .dispatch( 'core/block-editor' ) + .__unstableMarkNextChangeAsNotPersistent(); + registry + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( 'a', { foo: 2 } ); + + expect( onInput ).toHaveBeenCalledWith( + [ { clientId: 'a', innerBlocks: [], attributes: { foo: 2 } } ], + { selectionEnd: {}, selectionStart: {} } + ); + expect( onChange ).not.toHaveBeenCalled(); + } ); + + it( 'calls onChange if a persistent change occurs', async () => { + const onChange = jest.fn(); + const onInput = jest.fn(); + + const value1 = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + let registry; + const setRegistry = ( reg ) => { + registry = reg; + }; + await act( async () => { + create( + + ); + } ); + onChange.mockClear(); + onInput.mockClear(); + + // Create a persistent change. + registry + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( 'a', { foo: 2 } ); + + expect( onChange ).toHaveBeenCalledWith( + [ { clientId: 'a', innerBlocks: [], attributes: { foo: 2 } } ], + { selectionEnd: {}, selectionStart: {} } + ); + expect( onInput ).not.toHaveBeenCalled(); + } ); + + it( 'avoids updating the parent if there is a pending incoming change', async () => { + const replaceInnerBlocks = jest.spyOn( + blockEditorActions, + 'replaceInnerBlocks' + ); + + const onChange = jest.fn(); + const onInput = jest.fn(); + + const value1 = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + + await act( async () => { + create( + + ); + } ); + onChange.mockClear(); + onInput.mockClear(); + replaceInnerBlocks.mockClear(); + + await act( async () => { + create( + + ); + } ); + + expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'test', [], false ); + expect( onChange ).not.toHaveBeenCalled(); + expect( onInput ).not.toHaveBeenCalled(); + } ); + + it( 'avoids updating the block-editor store if there is a pending outgoint change', async () => { + const replaceInnerBlocks = jest.spyOn( + blockEditorActions, + 'replaceInnerBlocks' + ); + + const onChange = jest.fn(); + const onInput = jest.fn(); + + const value1 = [ + { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, + ]; + + let registry; + const setRegistry = ( reg ) => { + registry = reg; + }; + await act( async () => { + create( + + ); + } ); + onChange.mockClear(); + onInput.mockClear(); + replaceInnerBlocks.mockClear(); + + registry + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( 'a', { foo: 2 } ); + + expect( replaceInnerBlocks ).not.toHaveBeenCalled(); + expect( onChange ).toHaveBeenCalledWith( + [ { clientId: 'a', innerBlocks: [], attributes: { foo: 2 } } ], + { selectionEnd: {}, selectionStart: {} } + ); + expect( onInput ).not.toHaveBeenCalled(); + } ); +} );