diff --git a/__mocks__/react-native-webview.js b/__mocks__/react-native-webview.js new file mode 100644 index 0000000000..d80b7c32b2 --- /dev/null +++ b/__mocks__/react-native-webview.js @@ -0,0 +1,5 @@ +const React = require('react'); + +const WebView = (props) => React.createElement('WebView', props); + +module.exports = { WebView }; diff --git a/ios/BottomTabsTogetherAttacher.mm b/ios/BottomTabsTogetherAttacher.mm index b1fbfc4e7d..2b90e7eb0d 100644 --- a/ios/BottomTabsTogetherAttacher.mm +++ b/ios/BottomTabsTogetherAttacher.mm @@ -5,17 +5,39 @@ @implementation BottomTabsTogetherAttacher - (void)attach:(RNNBottomTabsController *)bottomTabsController { dispatch_group_t ready = dispatch_group_create(); - + + UIWindow *preloadWindow = [[UIWindow alloc] initWithFrame:CGRectZero]; + preloadWindow.hidden = NO; + + NSMapTable *reactViewToParent = [NSMapTable strongToStrongObjectsMapTable]; + for (UIViewController *vc in bottomTabsController.childViewControllers) { dispatch_group_enter(ready); [vc setReactViewReadyCallback:^{ - dispatch_group_leave(ready); + dispatch_group_leave(ready); }]; + [vc render]; + + if ([vc isKindOfClass:[UINavigationController class]]) { + UIView *containerView = [(UINavigationController *)vc topViewController].view; + UIView *reactView = containerView.subviews.firstObject; + + if (reactView && !reactView.window) { + [reactViewToParent setObject:containerView forKey:reactView]; + [preloadWindow addSubview:reactView]; + } + } } - + dispatch_notify(ready, dispatch_get_main_queue(), ^{ - [bottomTabsController readyForPresentation]; + for (UIView *reactView in reactViewToParent) { + UIView *parent = [reactViewToParent objectForKey:reactView]; + reactView.frame = parent.bounds; + [parent addSubview:reactView]; + } + preloadWindow.hidden = YES; //Keep preloadWindow reference alive to this point + [bottomTabsController readyForPresentation]; }); } diff --git a/jest-setup.js b/jest-setup.js index 4dddac7fb9..e7e2baa2b2 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -21,6 +21,7 @@ jest.mock('react-native-gesture-handler', () => { }; }); + mockDetox(() => require('./playground/index')); beforeEach(() => { diff --git a/jest.config.js b/jest.config.js index f0a2021b01..e41a3e24f9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { preset: 'react-native', transformIgnorePatterns: [ - 'node_modules/(?!(@react-native|react-native|react-native-ui-lib|react-native-animatable|react-native-reanimated)/)', + 'node_modules/(?!(@react-native|react-native|react-native-ui-lib|react-native-animatable|react-native-reanimated|react-native-webview)/)', ], transform: { '\\.[jt]sx?$': 'babel-jest', @@ -20,6 +20,7 @@ module.exports = { moduleNameMapper: { '^react-native$': '/node_modules/react-native', '^react-native-gesture-handler$': '/node_modules/react-native-gesture-handler', + '^react-native-webview$': '/__mocks__/react-native-webview.js', 'react-native-navigation/Mock': '/Mock/index', 'react-native-navigation': '/src', '^src$': '/src', diff --git a/package.json b/package.json index a8f52b6d8c..4b465c6fdb 100644 --- a/package.json +++ b/package.json @@ -182,4 +182,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/playground/e2e/TabbedWebViewScreen.test.js b/playground/e2e/TabbedWebViewScreen.test.js new file mode 100644 index 0000000000..1880e8f00b --- /dev/null +++ b/playground/e2e/TabbedWebViewScreen.test.js @@ -0,0 +1,22 @@ +import Utils from './Utils'; +import TestIDs from '../src/testIDs'; + +const { elementById, elementByLabel } = Utils; + +describe.e2e(':ios: Tabs with Together flag', () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await elementById(TestIDs.BOTTOM_TABS_BTN).tap(); + await expect(elementByLabel('First Tab')).toBeVisible(); + }); + + it('should load all tabs when tabsAttachMode is together', async () => { + await elementById(TestIDs.TABS_TOGETHER_BTN).tap(); + await waitFor(element(by.text(/\d→\d→\d/))) + .toExist() + .withTimeout(5000); + + await elementById(TestIDs.TABS_TOGETHER_DISMISS).tap(); + await expect(elementByLabel('First Tab')).toBeVisible(); + }); +}); diff --git a/playground/package.json b/playground/package.json index cf6aa58273..0ac73c43f8 100644 --- a/playground/package.json +++ b/playground/package.json @@ -21,6 +21,7 @@ "prop-types": "15.x.x", "react-lifecycles-compat": "^3.0.4", "react-native-redash": "^12.6.1", + "react-native-webview": "^13.12.5", "reanimated-color-picker": "^3.0.6", "ssim.js": "^3.5.0", "tslib": "1.9.3" diff --git a/playground/src/screens/FirstBottomTabScreen.tsx b/playground/src/screens/FirstBottomTabScreen.tsx index 61ca1c6f19..c8d1164c54 100644 --- a/playground/src/screens/FirstBottomTabScreen.tsx +++ b/playground/src/screens/FirstBottomTabScreen.tsx @@ -1,18 +1,19 @@ import React, { Component } from 'react'; -import { Text } from 'react-native'; +import { EmitterSubscription, Platform, Text } from 'react-native'; import { NavigationProps, Options } from 'react-native-navigation'; import Root from '../components/Root'; import Button from '../components/Button'; import Navigation from './../services/Navigation'; import Screens from './Screens'; -import { component } from '../commons/Layouts'; +import { stack, component } from '../commons/Layouts'; import testIDs from '../testIDs'; import bottomTabsStruct from './BottomTabsLayoutStructure'; +import { resetWebViewLoadedOrder, TAB_SCREENS } from './TabbedWebViewScreen'; export class MountedBottomTabScreensState { static mountedBottomTabScreens: string[] = []; - static callback: (mountedBottomTabScreens: string[]) => void = () => {}; + static callback: (mountedBottomTabScreens: string[]) => void = () => { }; static addScreen(screen: string) { this.mountedBottomTabScreens.push(screen); @@ -34,6 +35,7 @@ const { SCREEN_ROOT, SET_ROOT_BTN, BOTTOM_TABS, + TABS_TOGETHER_BTN, } = testIDs; interface NavigationState { @@ -74,9 +76,19 @@ export default class FirstBottomTabScreen extends Component { - if (event.tabIndex == 2) { - alert('BottomTabPressed'); + + registerBottomTabListener = () => { + return Navigation.events().registerBottomTabPressedListener((event) => { + if (event.tabIndex == 2) { + alert('BottomTabPressed'); + } + }); + }; + + bottomTabPressedListener: EmitterSubscription | null = this.registerBottomTabListener(); + modalDismissedListener = Navigation.events().registerModalDismissedListener((event) => { + if (event.componentId === 'TogetherFlagTabTest' && !this.bottomTabPressedListener) { + this.bottomTabPressedListener = this.registerBottomTabListener(); } }); @@ -93,6 +105,13 @@ export default class FirstBottomTabScreen extends Component + {Platform.OS === 'ios' && ( +