Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RNMobile] Refactor react-native-editor initialization test #37955

Merged
merged 9 commits into from
Jan 24, 2022
327 changes: 194 additions & 133 deletions packages/react-native-editor/src/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@
* External dependencies
*/
import { AppRegistry } from 'react-native';
import { render, waitFor } from 'test/helpers';
import { act, render } from 'test/helpers';

/**
* WordPress dependencies
*/
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
import * as wpHooks from '@wordpress/hooks';

/**
* Internal dependencies
*/
import { registerGutenberg } from '..';
import { registerGutenberg, initialHtmlGutenberg } from '..';
import setupLocale from '../setup-locale';

jest.mock( 'react-native/Libraries/ReactNative/AppRegistry' );
jest.mock( '../setup-locale' );

const initGutenberg = ( registerParams ) => {
const initGutenberg = ( registerParams, editorProps ) => {
let EditorComponent;
AppRegistry.registerComponent.mockImplementation(
( name, componentProvider ) => {
Expand All @@ -27,158 +28,218 @@ const initGutenberg = ( registerParams ) => {
);
registerGutenberg( registerParams );

return render( <EditorComponent /> );
let screen;
// This guarantees that setup module is imported on every test, as it's imported upon Editor component rendering.
jest.isolateModules( () => {
screen = render( <EditorComponent { ...editorProps } /> );
} );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this approach, we no longer need to reset modules for testing the call order of hooks and callbacks. Besides, I found out that resetting modules was breaking the test cases that render the editor.


return screen;
};

describe( 'Register Gutenberg', () => {
beforeEach( () => {
// We need to reset modules to guarantee that setup module is imported on every test.
jest.resetModules();
} );
Comment on lines -34 to -37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer necessary as the setup module import is guaranteed by using the jest.isolateModules function when rendering the editor.


it( 'registers Gutenberg editor component', () => {
registerGutenberg();
expect( AppRegistry.registerComponent ).toHaveBeenCalled();
} );

it( 'sets up locale before editor is initialized', () => {
const mockOnModuleImported = jest.fn();
jest.mock( '../setup', () => {
// To determine if the setup module is imported, we create a mock function that is called when the module is mocked.
mockOnModuleImported();

return {
__esModule: true,
default: jest.fn().mockReturnValue( <></> ),
};
describe( 'check calling order of hooks and callbacks', () => {
afterAll( () => {
// Revert setup mock to assure that rest of tests use original implementation.
jest.unmock( '../setup' );
} );

initGutenberg();

// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const setupLocaleCallOrder = setupLocale.mock.invocationCallOrder[ 0 ];
const onSetupImportedCallOrder =
mockOnModuleImported.mock.invocationCallOrder[ 0 ];
it( 'sets up locale before editor is initialized', () => {
const mockOnModuleImported = jest.fn();
jest.mock( '../setup', () => {
// To determine if the setup module is imported, we create a mock function that is called when the module is mocked.
mockOnModuleImported();

return {
__esModule: true,
default: jest.fn().mockReturnValue( <></> ),
};
} );

initGutenberg();

// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const setupLocaleCallOrder =
setupLocale.mock.invocationCallOrder[ 0 ];
const onSetupImportedCallOrder =
mockOnModuleImported.mock.invocationCallOrder[ 0 ];

expect( setupLocaleCallOrder ).toBeLessThan(
onSetupImportedCallOrder
);
} );

expect( setupLocaleCallOrder ).toBeLessThan( onSetupImportedCallOrder );
} );
it( 'beforeInit callback is invoked before the editor is initialized', () => {
const beforeInitCallback = jest.fn();
const mockOnModuleImported = jest.fn();
jest.mock( '../setup', () => {
// To determine if the setup module is imported, we create a mock function that is called when the module is mocked.
mockOnModuleImported();

return {
__esModule: true,
default: jest.fn().mockReturnValue( <></> ),
};
} );

initGutenberg( { beforeInitCallback } );

// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const beforeInitCallOrder =
beforeInitCallback.mock.invocationCallOrder[ 0 ];
const onSetupImportedCallOrder =
mockOnModuleImported.mock.invocationCallOrder[ 0 ];

expect( beforeInitCallOrder ).toBeLessThan(
onSetupImportedCallOrder
);
} );

it( 'beforeInit callback is invoked before the editor is initialized', () => {
const beforeInitCallback = jest.fn();
const mockOnModuleImported = jest.fn();
jest.mock( '../setup', () => {
// To determine if the setup module is imported, we create a mock function that is called when the module is mocked.
mockOnModuleImported();
it( 'dispatches "native.pre-render" hook before the editor is rendered', () => {
const doAction = jest.spyOn( wpHooks, 'doAction' );

return {
__esModule: true,
default: jest.fn().mockReturnValue( <></> ),
// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 0;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
doAction.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = doAction.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.pre-render' );
expect( hookCallOrder ).toBeLessThan( onRenderEditorCallOrder );
} );

initGutenberg( { beforeInitCallback } );
it( 'dispatches "native.block_editor_props" hook before the editor is rendered', () => {
const applyFilters = jest.spyOn( wpHooks, 'applyFilters' );

// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const beforeInitCallOrder =
beforeInitCallback.mock.invocationCallOrder[ 0 ];
const onSetupImportedCallOrder =
mockOnModuleImported.mock.invocationCallOrder[ 0 ];
// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 0;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
applyFilters.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = applyFilters.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.block_editor_props' );
expect( hookCallOrder ).toBeLessThan( onRenderEditorCallOrder );
} );

expect( beforeInitCallOrder ).toBeLessThan( onSetupImportedCallOrder );
} );
it( 'dispatches "native.render" hook after the editor is rendered', () => {
const doAction = jest.spyOn( wpHooks, 'doAction' );

it( 'dispatches "native.pre-render" hook before the editor is rendered', () => {
const doAction = jest.spyOn( wpHooks, 'doAction' );

// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 0;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
doAction.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = doAction.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.pre-render' );
expect( hookCallOrder ).toBeLessThan( onRenderEditorCallOrder );
// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 1;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
doAction.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = doAction.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.render' );
expect( hookCallOrder ).toBeGreaterThan( onRenderEditorCallOrder );
} );
} );

it( 'dispatches "native.block_editor_props" hook before the editor is rendered', () => {
const applyFilters = jest.spyOn( wpHooks, 'applyFilters' );

// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 0;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
applyFilters.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = applyFilters.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.block_editor_props' );
expect( hookCallOrder ).toBeLessThan( onRenderEditorCallOrder );
} );
describe( 'editor initialization', () => {
beforeEach( () => {
// Setup already registers blocks so we need assure that no blocks are registered before the test.
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );

it( 'dispatches "native.render" hook after the editor is rendered', () => {
const doAction = jest.spyOn( wpHooks, 'doAction' );

// An empty component is provided in order to listen for render calls of the editor component.
const onRenderEditor = jest.fn();
const EditorComponent = () => {
onRenderEditor();
return null;
};
jest.mock( '../setup', () => ( {
__esModule: true,
default: jest.fn().mockReturnValue( <EditorComponent /> ),
} ) );

initGutenberg();

const hookCallIndex = 1;
// "invocationCallOrder" can be used to compare call orders between different mocks.
// Reference: https://git.io/JyBk0
const hookCallOrder =
doAction.mock.invocationCallOrder[ hookCallIndex ];
const onRenderEditorCallOrder =
onRenderEditor.mock.invocationCallOrder[ 0 ];
const hookName = doAction.mock.calls[ hookCallIndex ][ 0 ];

expect( hookName ).toBe( 'native.render' );
expect( hookCallOrder ).toBeGreaterThan( onRenderEditorCallOrder );
} );
it( 'initializes the editor with empty HTML', async () => {
const consoleLog = jest
.spyOn( console, 'log' )
.mockImplementation( jest.fn() );

const { getByTestId } = initGutenberg( {}, { initialData: '' } );
// Some of the store updates that happen upon editor initialization are executed at the end of the current
// Javascript block execution and after the test is finished. In order to prevent "act" warnings due to
// this behavior, we wait for the execution block to be finished before acting on the test.
await act(
() => new Promise( ( resolve ) => setImmediate( resolve ) )
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't wait for the JS block to be finished, we get a bunch of act warnings:

Warning: An update to EditorProvider inside a test was not wrapped in act(...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a note that we are actively investigating this issue and approach further in #38052. We should likely retroactively apply whatever global solution we find there to this test.

const blockList = getByTestId( 'block-list-wrapper' );

expect( blockList ).toHaveProperty( 'type', 'View' );
expect( consoleLog ).toHaveBeenCalledWith( 'Hermes is: true' );
} );

it( 'initializes the editor', () => {
const { getByTestId } = initGutenberg();
const blockList = waitFor( () => getByTestId( 'block-list-wrapper' ) );
expect( blockList ).toBeDefined();
it( 'initializes the editor with initial HTML', async () => {
const consoleLog = jest
.spyOn( console, 'log' )
.mockImplementation( jest.fn() );
const consoleInfo = jest
.spyOn( console, 'info' )
.mockImplementation( jest.fn() );

const { getByTestId } = initGutenberg(
{},
{ initialData: initialHtmlGutenberg }
);
// Some of the store updates that happen upon editor initialization are executed at the end of the current
// Javascript block execution and after the test is finished. In order to prevent "act" warnings due to
// this behavior, we wait for the execution block to be finished before acting on the test.
await act(
() => new Promise( ( resolve ) => setImmediate( resolve ) )
);
const blockList = getByTestId( 'block-list-wrapper' );

expect( blockList ).toHaveProperty( 'type', 'View' );
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
expect( consoleLog ).toHaveBeenCalledWith( 'Hermes is: true' );
// It's expected that some blocks are upgraded and inform about it (example: "Updated Block: core/cover")
expect( consoleInfo ).toHaveBeenCalled();
} );
} );
} );