From b7b62d2d3db4395ce5d2ad42bd1fc0f1c3c2f12d Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 4 Jan 2022 16:27:31 +0100 Subject: [PATCH] [RNMobile] Refactor native editor initialization (#37073) * Refactor editor initialization * Update setup locale to be generic * Fix globals import * Trigger init callback before editor initialization * Update package-lock.json * Set forceRTL in react native setup function * Add plugin translations to setup locale function * Remove block-library lazy import * Remove domain default value * Add plugin translations to Gutenberg initialization * Document register Gutenberg function * Update Gutenberg constructor * Add default values for register Gutenberg * Add locale initial prop to Gutenberg demo * Update react-native-editor changelog * Add note to react-native-editor index * Move locale prop definition * Move lodash import to external deps * Remove lodash usage from react-native-editor * Refactor setup locale with code improvements * Reorder imports in react-native-editor setup * Move variable definitions close to their functions * Inline editor initialization call * Update setup locale comment * Add default value to setup locale * Add setup locale tests * Add test cases for register gutenberg * Add editor initialization test case * Revert api-fetch mock * Mock setFetchHandler of api-fetch package * Mock setFetchHandler within mock block * Add eslint rule override in react-native-editor This override matches the same behavior we have in root ESLint configuration. Reference: https://github.com/WordPress/gutenberg/blob/ca85ced9298a252d4832438d8408ea4cda95696f/.eslintrc.js#L171-L185 * Add word mock to onModuleImported variables --- packages/edit-post/src/index.native.js | 11 +- packages/element/src/react-platform.native.js | 50 ----- .../mobile/WPAndroidGlue/GutenbergProps.kt | 2 +- packages/react-native-editor/.eslintrc.js | 8 + packages/react-native-editor/CHANGELOG.md | 1 + .../main/java/com/gutenberg/MainActivity.java | 9 + packages/react-native-editor/index.js | 5 +- packages/react-native-editor/src/index.js | 207 +++++------------- .../react-native-editor/src/setup-locale.js | 57 +++++ packages/react-native-editor/src/setup.js | 145 ++++++++++++ .../src/test/index.test.js | 184 ++++++++++++++++ .../src/test/setup-locale.test.js | 55 +++++ test/native/setup.js | 7 +- 13 files changed, 530 insertions(+), 211 deletions(-) delete mode 100644 packages/element/src/react-platform.native.js create mode 100644 packages/react-native-editor/src/setup-locale.js create mode 100644 packages/react-native-editor/src/setup.js create mode 100644 packages/react-native-editor/src/test/index.test.js create mode 100644 packages/react-native-editor/src/test/setup-locale.test.js diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 60548d4cdf4c31..609df65e3bb6a5 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -3,7 +3,6 @@ */ import '@wordpress/core-data'; import '@wordpress/format-library'; -import { render } from '@wordpress/element'; /** * Internal dependencies @@ -11,8 +10,6 @@ import { render } from '@wordpress/element'; export { store } from './store'; import Editor from './editor'; -let editorInitialized = false; - /** * Initializes the Editor and returns a componentProvider * that can be registered with `AppRegistry.registerComponent` @@ -22,11 +19,5 @@ let editorInitialized = false; * @param {Object} postId ID of the post to edit (unused right now) */ export function initializeEditor( id, postType, postId ) { - if ( editorInitialized ) { - return; - } - - editorInitialized = true; - - render( , id ); + return ; } diff --git a/packages/element/src/react-platform.native.js b/packages/element/src/react-platform.native.js deleted file mode 100644 index adaedd2b776875..00000000000000 --- a/packages/element/src/react-platform.native.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * External dependencies - */ -import { AppRegistry } from 'react-native'; -import { omit } from 'lodash'; - -/** - * WordPress dependencies - */ -import { applyFilters, doAction } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { Component, cloneElement } from './react'; - -const render = ( element, id ) => { - class App extends Component { - constructor() { - super( ...arguments ); - - const parentProps = omit( this.props || {}, [ 'rootTag' ] ); - - doAction( 'native.pre-render', parentProps ); - - this.filteredProps = applyFilters( - 'native.block_editor_props', - parentProps - ); - } - - componentDidMount() { - doAction( 'native.render', this.filteredProps ); - } - - render() { - return cloneElement( element, this.filteredProps ); - } - } - - AppRegistry.registerComponent( id, () => App ); -}; - -/** - * Render a given element on Native. - * This actually returns a componentProvider that can be registered with `AppRegistry.registerComponent` - * - * @param {WPElement} element Element to render. - */ -export { render }; diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index def0ec98d51708..2b406ab0ae4b64 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -79,7 +79,6 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_INITIAL_HTML_MODE_ENABLED = "initialHtmlModeEnabled" private const val PROP_POST_TYPE = "postType" private const val PROP_INITIAL_FEATURED_IMAGE_ID = "featuredImageId" - private const val PROP_LOCALE = "locale" private const val PROP_TRANSLATIONS = "translations" private const val PROP_COLORS = "colors" private const val PROP_GRADIENTS = "gradients" @@ -88,6 +87,7 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_IS_FSE_THEME = "isFSETheme" private const val PROP_GALLERY_WITH_IMAGE_BLOCKS = "galleryWithImageBlocks" + const val PROP_LOCALE = "locale" const val PROP_CAPABILITIES = "capabilities" const val PROP_CAPABILITIES_CONTACT_INFO_BLOCK = "contactInfoBlock" const val PROP_CAPABILITIES_LAYOUT_GRID_BLOCK = "layoutGridBlock" diff --git a/packages/react-native-editor/.eslintrc.js b/packages/react-native-editor/.eslintrc.js index 9605a6d48f1d47..bbda54abfe7506 100644 --- a/packages/react-native-editor/.eslintrc.js +++ b/packages/react-native-editor/.eslintrc.js @@ -67,4 +67,12 @@ module.exports = { }, ], }, + overrides: [ + { + files: [ '**/*.js' ], + rules: { + 'import/no-unresolved': 'off', + }, + }, + ], }; diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 8ada7d0fe5eb23..ee7cf9e00cd227 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Give multi-line block names central alignment in inserter [#37185] - [**] Fix empty line apperaing when splitting heading blocks on Android 12 [#37279] +- [**] Fix missing translations by refactoring the editor initialization code [#37073] ## 1.68.0 - [**] Fix undo/redo functionality in links when applying text format [#36861] diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 5ec074f471ec5e..dcc86c5246b365 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -9,6 +9,8 @@ import org.wordpress.mobile.WPAndroidGlue.GutenbergProps; +import java.util.Locale; + public class MainActivity extends ReactActivity { /** @@ -27,6 +29,13 @@ protected ReactActivityDelegate createReactActivityDelegate() { @Override protected Bundle getLaunchOptions() { Bundle bundle = new Bundle(); + + // Add locale + String languageString = Locale.getDefault().toString(); + String localeSlug = languageString.replace("_", "-").toLowerCase(Locale.ENGLISH); + bundle.putString(GutenbergProps.PROP_LOCALE, localeSlug); + + // Add capabilities Bundle capabilities = new Bundle(); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_MENTIONS, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_XPOSTS, true); diff --git a/packages/react-native-editor/index.js b/packages/react-native-editor/index.js index d8098ce323f6ab..3aaaf04720d6e2 100644 --- a/packages/react-native-editor/index.js +++ b/packages/react-native-editor/index.js @@ -2,9 +2,10 @@ * External dependencies */ import 'react-native-gesture-handler'; + /** * Internal dependencies */ -import { doGutenbergNativeSetup } from './src'; +import { registerGutenberg } from './src'; -doGutenbergNativeSetup(); +registerGutenberg(); diff --git a/packages/react-native-editor/src/index.js b/packages/react-native-editor/src/index.js index bc8825c7113be5..cb3128ea5cf7c6 100644 --- a/packages/react-native-editor/src/index.js +++ b/packages/react-native-editor/src/index.js @@ -1,170 +1,83 @@ /** * External dependencies */ -import { I18nManager, LogBox } from 'react-native'; +import 'react-native-gesture-handler'; +import { AppRegistry } from 'react-native'; + +/** + * WordPress dependencies + */ +import { applyFilters, doAction } from '@wordpress/hooks'; +import { Component, cloneElement } from '@wordpress/element'; /** * Internal dependencies */ import './globals'; -import { getTranslation } from '../i18n-cache'; import initialHtml from './initial-html'; -import setupApiFetch from './api-fetch-setup'; +import setupLocale from './setup-locale'; +import { getTranslation as getGutenbergTranslation } from '../i18n-cache'; /** - * WordPress dependencies + * Register Gutenberg editor to React Native App registry. + * + * @typedef {Object} PluginTranslation + * @property {string} domain Domain of the plugin. + * @property {Function} getTranslation Function for retrieving translations for a locale. + * + * @param {Object} arguments + * @param {Function} arguments.beforeInitCallback Callback executed before the editor initialization. + * @param {PluginTranslation[]} arguments.pluginTranslations Array with plugin translations. */ -import { - validateThemeColors, - validateThemeGradients, -} from '@wordpress/block-editor'; -import { unregisterBlockType, getBlockType } from '@wordpress/blocks'; - -const reactNativeSetup = () => { - LogBox.ignoreLogs( [ - 'Require cycle:', // TODO: Refactor to remove require cycles - 'lineHeight', // TODO: Remove lineHeight warning from Aztec - /** - * TODO: Migrate to @gorhom/bottom-sheet or replace usage of - * LayoutAnimation to Animated. KeyboardAvoidingView's usage of - * LayoutAnimation collides with both BottomSheet and NavigationContainer - * usage of LayoutAnimation simultaneously https://git.io/J1lZv, - * https://git.io/J1lZY - */ - 'Overriding previous layout animation', - ] ); - - I18nManager.forceRTL( false ); // Change to `true` to debug RTL layout easily. -}; - -const gutenbergSetup = () => { - const wpData = require( '@wordpress/data' ); - - // wp-data - const userId = 1; - const storageKey = 'WP_DATA_USER_' + userId; - wpData.use( wpData.plugins.persistence, { storageKey } ); - - setupApiFetch(); - - const isHermes = () => global.HermesInternal !== null; - // eslint-disable-next-line no-console - console.log( 'Hermes is: ' + isHermes() ); - - setupInitHooks(); - - const initializeEditor = require( '@wordpress/edit-post' ).initializeEditor; - initializeEditor( 'gutenberg', 'post', 1 ); -}; - -const setupInitHooks = () => { - const wpHooks = require( '@wordpress/hooks' ); - - wpHooks.addAction( - 'native.pre-render', - 'core/react-native-editor', - ( props ) => { - setupLocale( props.locale, props.translations ); - - const capabilities = props.capabilities ?? {}; - if ( - getBlockType( 'core/block' ) !== undefined && - capabilities.reusableBlock !== true - ) { - unregisterBlockType( 'core/block' ); - } - } - ); - - // Map native props to Editor props - // TODO: normalize props in the bridge (So we don't have to map initialData to initialHtml) - wpHooks.addFilter( - 'native.block_editor_props', - 'core/react-native-editor', - ( props ) => { - const { capabilities = {} } = props; - let { - initialData, - initialTitle, - postType, - featuredImageId, - colors, - gradients, - rawStyles, - rawFeatures, - galleryWithImageBlocks, - locale, - } = props; - - if ( initialData === undefined && __DEV__ ) { - initialData = initialHtml; - } - if ( initialTitle === undefined ) { - initialTitle = 'Welcome to Gutenberg!'; - } - if ( postType === undefined ) { - postType = 'post'; +const registerGutenberg = ( { + beforeInitCallback, + pluginTranslations = [], +} = {} ) => { + class Gutenberg extends Component { + constructor( props ) { + super( props ); + + // eslint-disable-next-line no-unused-vars + const { rootTag, ...parentProps } = this.props; + + // Setup locale + setupLocale( + parentProps.locale, + parentProps.translations, + getGutenbergTranslation, + pluginTranslations + ); + + if ( beforeInitCallback ) { + beforeInitCallback( parentProps ); } - colors = validateThemeColors( colors ); + // We have to lazy import the setup code to prevent executing any code located + // at global scope before the editor is initialized, like translations retrieval. + const setup = require( './setup' ).default; + // Initialize editor + this.editorComponent = setup(); - gradients = validateThemeGradients( gradients ); + // Dispatch pre-render hooks + doAction( 'native.pre-render', parentProps ); - return { - initialHtml: initialData, - initialHtmlModeEnabled: props.initialHtmlModeEnabled, - initialTitle, - postType, - featuredImageId, - capabilities, - colors, - gradients, - rawStyles, - rawFeatures, - galleryWithImageBlocks, - locale, - }; + this.filteredProps = applyFilters( + 'native.block_editor_props', + parentProps + ); } - ); -}; - -let blocksRegistered = false; - -const setupLocale = ( locale, extraTranslations ) => { - const setLocaleData = require( '@wordpress/i18n' ).setLocaleData; - - I18nManager.forceRTL( false ); // Change to `true` to debug RTL layout easily. - let gutenbergTranslations = getTranslation( locale ); - if ( locale && ! gutenbergTranslations ) { - // Try stripping out the regional - locale = locale.replace( /[-_][A-Za-z]+$/, '' ); - gutenbergTranslations = getTranslation( locale ); - } - const translations = Object.assign( - {}, - gutenbergTranslations, - extraTranslations - ); - // eslint-disable-next-line no-console - console.log( 'locale', locale, translations ); - // Only change the locale if it's supported by gutenberg - if ( gutenbergTranslations || extraTranslations ) { - setLocaleData( translations ); - } + componentDidMount() { + // Dispatch post-render hooks + doAction( 'native.render', this.filteredProps ); + } - if ( blocksRegistered ) { - return; + render() { + return cloneElement( this.editorComponent, this.filteredProps ); + } } - const registerCoreBlocks = require( '@wordpress/block-library' ) - .registerCoreBlocks; - registerCoreBlocks(); - blocksRegistered = true; + AppRegistry.registerComponent( 'gutenberg', () => Gutenberg ); }; -export { initialHtml as initialHtmlGutenberg }; -export function doGutenbergNativeSetup() { - reactNativeSetup(); - gutenbergSetup(); -} +export { initialHtml as initialHtmlGutenberg, registerGutenberg, setupLocale }; diff --git a/packages/react-native-editor/src/setup-locale.js b/packages/react-native-editor/src/setup-locale.js new file mode 100644 index 00000000000000..c4b222b54f8518 --- /dev/null +++ b/packages/react-native-editor/src/setup-locale.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +import { setLocaleData } from '@wordpress/i18n'; + +/** + * Setup locale data for default domain and plugins. + * + * @typedef {Object} PluginTranslation + * @property {string} domain Domain of the plugin. + * @property {Function} getTranslation Function for retrieving translations for a locale. + * + * @param {string} locale Locale value. + * @param {Object} extraTranslations Extra translations to be included. + * @param {Function} getDefaultTranslation Default domain's function for retrieving translations for a locale. + * @param {PluginTranslation[]} pluginTranslations Array with plugin translations. + */ +export default ( + locale, + extraTranslations, + getDefaultTranslation, + pluginTranslations = [] +) => { + const setDomainLocaleData = ( { getTranslation, domain = 'default' } ) => { + let translations = getTranslation( locale ); + if ( locale && ! translations ) { + // Try stripping out the regional + locale = locale.replace( /[-_][A-Za-z]+$/, '' ); + translations = getTranslation( locale ); + } + const allTranslations = { + ...translations, + ...extraTranslations, + }; + + if ( domain === 'default' ) { + // eslint-disable-next-line no-console + console.log( 'locale', locale, allTranslations ); + } else { + // Extra translations are already logged along with the default domain, so + // for other domains we can limit the output to their translations. + // eslint-disable-next-line no-console + console.log( `${ domain } - locale`, locale, translations ); + } + + // Only change the locale if it's supported by gutenberg + if ( translations || extraTranslations ) { + setLocaleData( allTranslations, domain ); + } + }; + + // Set up default domain and plugin translations + [ + { getTranslation: getDefaultTranslation }, + ...pluginTranslations, + ].forEach( setDomainLocaleData ); +}; diff --git a/packages/react-native-editor/src/setup.js b/packages/react-native-editor/src/setup.js new file mode 100644 index 00000000000000..727dd39440fea4 --- /dev/null +++ b/packages/react-native-editor/src/setup.js @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import { I18nManager, LogBox } from 'react-native'; + +/** + * WordPress dependencies + */ +import { + validateThemeColors, + validateThemeGradients, +} from '@wordpress/block-editor'; +import { unregisterBlockType, getBlockType } from '@wordpress/blocks'; +import { addAction, addFilter } from '@wordpress/hooks'; +import * as wpData from '@wordpress/data'; +import { initializeEditor } from '@wordpress/edit-post'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +/** + * Internal dependencies + */ +import initialHtml from './initial-html'; +import setupApiFetch from './api-fetch-setup'; + +const reactNativeSetup = () => { + LogBox.ignoreLogs( [ + 'Require cycle:', // TODO: Refactor to remove require cycles + 'lineHeight', // TODO: Remove lineHeight warning from Aztec + /** + * TODO: Migrate to @gorhom/bottom-sheet or replace usage of + * LayoutAnimation to Animated. KeyboardAvoidingView's usage of + * LayoutAnimation collides with both BottomSheet and NavigationContainer + * usage of LayoutAnimation simultaneously https://git.io/J1lZv, + * https://git.io/J1lZY + */ + 'Overriding previous layout animation', + ] ); + + I18nManager.forceRTL( false ); // Change to `true` to debug RTL layout easily. +}; + +const gutenbergSetup = () => { + // wp-data + const userId = 1; + const storageKey = 'WP_DATA_USER_' + userId; + wpData.use( wpData.plugins.persistence, { storageKey } ); + + setupApiFetch(); + + const isHermes = () => global.HermesInternal !== null; + // eslint-disable-next-line no-console + console.log( 'Hermes is: ' + isHermes() ); + + setupInitHooks(); +}; + +const setupInitHooks = () => { + addAction( 'native.pre-render', 'core/react-native-editor', ( props ) => { + registerBlocks(); + + const capabilities = props.capabilities ?? {}; + // Unregister non-supported blocks by capabilities + if ( + getBlockType( 'core/block' ) !== undefined && + capabilities.reusableBlock !== true + ) { + unregisterBlockType( 'core/block' ); + } + } ); + + // Map native props to Editor props + // TODO: normalize props in the bridge (So we don't have to map initialData to initialHtml) + addFilter( + 'native.block_editor_props', + 'core/react-native-editor', + ( props ) => { + const { capabilities = {} } = props; + let { + initialData, + initialTitle, + postType, + featuredImageId, + colors, + gradients, + rawStyles, + rawFeatures, + galleryWithImageBlocks, + locale, + } = props; + + if ( initialData === undefined && __DEV__ ) { + initialData = initialHtml; + } + if ( initialTitle === undefined ) { + initialTitle = 'Welcome to Gutenberg!'; + } + if ( postType === undefined ) { + postType = 'post'; + } + + colors = validateThemeColors( colors ); + + gradients = validateThemeGradients( gradients ); + + return { + initialHtml: initialData, + initialHtmlModeEnabled: props.initialHtmlModeEnabled, + initialTitle, + postType, + featuredImageId, + capabilities, + colors, + gradients, + rawStyles, + rawFeatures, + galleryWithImageBlocks, + locale, + }; + } + ); +}; + +let blocksRegistered = false; +const registerBlocks = () => { + if ( blocksRegistered ) { + return; + } + + registerCoreBlocks(); + + blocksRegistered = true; +}; + +let editorComponent; +export default () => { + if ( editorComponent ) { + return editorComponent; + } + + reactNativeSetup(); + gutenbergSetup(); + editorComponent = initializeEditor( 'gutenberg', 'post', 1 ); + + return editorComponent; +}; diff --git a/packages/react-native-editor/src/test/index.test.js b/packages/react-native-editor/src/test/index.test.js new file mode 100644 index 00000000000000..c734603279ea45 --- /dev/null +++ b/packages/react-native-editor/src/test/index.test.js @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { AppRegistry } from 'react-native'; +import { render, waitFor } from 'test/helpers'; + +/** + * WordPress dependencies + */ +import * as wpHooks from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { registerGutenberg } from '..'; +import setupLocale from '../setup-locale'; + +jest.mock( 'react-native/Libraries/ReactNative/AppRegistry' ); +jest.mock( '../setup-locale' ); + +const initGutenberg = ( registerParams ) => { + let EditorComponent; + AppRegistry.registerComponent.mockImplementation( + ( name, componentProvider ) => { + EditorComponent = componentProvider(); + } + ); + registerGutenberg( registerParams ); + + return render( ); +}; + +describe( 'Register Gutenberg', () => { + beforeEach( () => { + // We need to reset modules to guarantee that setup module is imported on every test. + jest.resetModules(); + } ); + + 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( <> ), + }; + } ); + + 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 ); + } ); + + 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( '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( ), + } ) ); + + 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 ); + } ); + + 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( ), + } ) ); + + 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 ); + } ); + + 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( ), + } ) ); + + 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', () => { + const { getByTestId } = initGutenberg(); + const blockList = waitFor( () => getByTestId( 'block-list-wrapper' ) ); + expect( blockList ).toBeDefined(); + } ); +} ); diff --git a/packages/react-native-editor/src/test/setup-locale.test.js b/packages/react-native-editor/src/test/setup-locale.test.js new file mode 100644 index 00000000000000..fe25844ed5da50 --- /dev/null +++ b/packages/react-native-editor/src/test/setup-locale.test.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import setupLocale from '../setup-locale'; + +const getDefaultTranslation = () => ( { + 'default-string': [ 'default-string-translation' ], +} ); + +const extraTranslations = { + 'extra-string': [ 'extra-string-translation' ], +}; + +const pluginTranslations = [ + { + domain: 'domain-1', + getTranslation: () => ( { + 'domain-1-string': [ 'domain-1-string-translation' ], + } ), + }, +]; + +describe( 'Setup locale', () => { + it( 'sets up default domain translations', () => { + setupLocale( 'test', extraTranslations, getDefaultTranslation ); + + expect( __( 'default-string' ) ).toBe( 'default-string-translation' ); + expect( __( 'extra-string' ) ).toBe( 'extra-string-translation' ); + } ); + + it( 'sets up plugin translations', () => { + const domain = 'domain-1'; + + setupLocale( + 'test', + extraTranslations, + getDefaultTranslation, + pluginTranslations + ); + + /* eslint-disable @wordpress/i18n-text-domain */ + expect( __( 'domain-1-string', domain ) ).toBe( + 'domain-1-string-translation' + ); + expect( __( 'extra-string', domain ) ).toBe( + 'extra-string-translation' + ); + /* eslint-enable @wordpress/i18n-text-domain */ + } ); +} ); diff --git a/test/native/setup.js b/test/native/setup.js index 86a01a2535370d..d1d26bc05773d7 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -34,7 +34,12 @@ jest.mock( '@wordpress/element', () => { }; } ); -jest.mock( '@wordpress/api-fetch', () => jest.fn() ); +jest.mock( '@wordpress/api-fetch', () => { + const apiFetchMock = jest.fn(); + apiFetchMock.setFetchHandler = jest.fn(); + + return apiFetchMock; +} ); jest.mock( '@wordpress/react-native-bridge', () => { return {