Skip to content

Commit

Permalink
External component lifecycle events (#4872)
Browse files Browse the repository at this point in the history
* Add external component e2e
* Emit appear and disappear events for external components
  • Loading branch information
guyca authored Mar 17, 2019
1 parent 4517d22 commit 602c669
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 68 deletions.
23 changes: 23 additions & 0 deletions e2e/ExternalComponent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const Utils = require('./Utils');
const TestIDs = require('../playground/src/testIDs');
const Android = require('./AndroidUtils');

const { elementByLabel, elementById } = Utils;

describe('External Component', () => {
beforeEach(async () => {
await device.relaunchApp();
await elementById(TestIDs.NAVIGATION_TAB).tap();
await elementById(TestIDs.EXTERNAL_COMP_BTN).tap();
});

test('Push external component', async () => {
await elementById(TestIDs.PUSH_BTN).tap();
await expect(elementByLabel('This is an external component')).toBeVisible();
});

test('Show external component in deep stack in modal', async () => {
await elementById(TestIDs.MODAL_BTN).tap();
await expect(elementByLabel('External component in deep stack')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private ViewController createExternalComponent(LayoutNode node) {
externalComponent,
externalComponentCreators.get(externalComponent.name.get()),
reactInstanceManager,
new EventEmitter(reactInstanceManager.getCurrentReactContext()),
parse(typefaceManager, node.getOptions())
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,69 @@
package com.reactnativenavigation.react;

import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import javax.annotation.Nullable;

import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;

public class EventEmitter {
private static final String AppLaunched = "RNN.AppLaunched";
private static final String CommandCompleted = "RNN.CommandCompleted";
private static final String BottomTabSelected = "RNN.BottomTabSelected";
private static final String ComponentDidAppear = "RNN.ComponentDidAppear";
private static final String ComponentDidDisappear = "RNN.ComponentDidDisappear";
private static final String NavigationButtonPressed = "RNN.NavigationButtonPressed";
private static final String ModalDismissed = "RNN.ModalDismissed";

private final RCTDeviceEventEmitter emitter;
private static final String AppLaunched = "RNN.AppLaunched";
private static final String CommandCompleted = "RNN.CommandCompleted";
private static final String BottomTabSelected = "RNN.BottomTabSelected";
private static final String ComponentDidAppear = "RNN.ComponentDidAppear";
private static final String ComponentDidDisappear = "RNN.ComponentDidDisappear";
private static final String NavigationButtonPressed = "RNN.NavigationButtonPressed";
private static final String ModalDismissed = "RNN.ModalDismissed";
@Nullable
private ReactContext reactContext;

public EventEmitter(ReactContext reactContext) {
this.emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
}
public EventEmitter(@Nullable ReactContext reactContext) {
this.reactContext = reactContext;
}

public void appLaunched() {
emit(AppLaunched);
}
public void appLaunched() {
emit(AppLaunched);
}

public void componentDidDisappear(String id, String componentName) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("componentName", componentName);
emit(ComponentDidDisappear, event);
}
public void emitComponentDidDisappear(String id, String componentName) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("componentName", componentName);
emit(ComponentDidDisappear, event);
}

public void componentDidAppear(String id, String componentName) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("componentName", componentName);
emit(ComponentDidAppear, event);
}
public void emitComponentDidAppear(String id, String componentName) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("componentName", componentName);
emit(ComponentDidAppear, event);
}

public void emitOnNavigationButtonPressed(String id, String buttonId) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("buttonId", buttonId);
emit(NavigationButtonPressed, event);
}
public void emitOnNavigationButtonPressed(String id, String buttonId) {
WritableMap event = Arguments.createMap();
event.putString("componentId", id);
event.putString("buttonId", buttonId);
emit(NavigationButtonPressed, event);
}

public void emitBottomTabSelected(int unselectedTabIndex, int selectedTabIndex) {
WritableMap event = Arguments.createMap();
event.putInt("unselectedTabIndex", unselectedTabIndex);
event.putInt("selectedTabIndex", selectedTabIndex);
emit(BottomTabSelected, event);
}
public void emitBottomTabSelected(int unselectedTabIndex, int selectedTabIndex) {
WritableMap event = Arguments.createMap();
event.putInt("unselectedTabIndex", unselectedTabIndex);
event.putInt("selectedTabIndex", selectedTabIndex);
emit(BottomTabSelected, event);
}

public void emitCommandCompleted(String commandId, long completionTime) {
WritableMap event = Arguments.createMap();
event.putString("commandId", commandId);
event.putDouble("completionTime", completionTime);
emit(CommandCompleted, event);
}
public void emitCommandCompleted(String commandId, long completionTime) {
WritableMap event = Arguments.createMap();
event.putString("commandId", commandId);
event.putDouble("completionTime", completionTime);
emit(CommandCompleted, event);
}

public void emitModalDismissed(String id, int modalsDismissed) {
WritableMap event = Arguments.createMap();
Expand All @@ -68,11 +72,16 @@ public void emitModalDismissed(String id, int modalsDismissed) {
emit(ModalDismissed, event);
}

private void emit(String eventName) {
emit(eventName, Arguments.createMap());
}
private void emit(String eventName) {
emit(eventName, Arguments.createMap());
}

private void emit(String eventName, WritableMap data) {
emitter.emit(eventName, data);
}
private void emit(String eventName, WritableMap data) {
if (reactContext == null) {
Log.e("RNN", "Could not send event " + eventName + ". React context is null!");
return;
}
RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
emitter.emit(eventName, data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ public void destroy() {
public void sendComponentStart() {
ReactContext currentReactContext = reactInstanceManager.getCurrentReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).componentDidAppear(componentId, componentName);
new EventEmitter(currentReactContext).emitComponentDidAppear(componentId, componentName);
}
}

@Override
public void sendComponentStop() {
ReactContext currentReactContext = reactInstanceManager.getCurrentReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).componentDidDisappear(componentId, componentName);
new EventEmitter(currentReactContext).emitComponentDidDisappear(componentId, componentName);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.support.v4.app.FragmentActivity;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.reactnativenavigation.parse.ExternalComponent;
import com.reactnativenavigation.parse.Options;
import com.reactnativenavigation.react.EventEmitter;
Expand All @@ -16,27 +15,28 @@ public class ExternalComponentViewController extends ViewController<ExternalComp
private final ExternalComponent externalComponent;
private final ExternalComponentCreator componentCreator;
private ReactInstanceManager reactInstanceManager;
private final EventEmitter emitter;

public ExternalComponentViewController(Activity activity, String id, ExternalComponent externalComponent, ExternalComponentCreator componentCreator, ReactInstanceManager reactInstanceManager, Options initialOptions) {
public ExternalComponentViewController(Activity activity, String id, ExternalComponent externalComponent, ExternalComponentCreator componentCreator, ReactInstanceManager reactInstanceManager, EventEmitter emitter, Options initialOptions) {
super(activity, id, new NoOpYellowBoxDelegate(), initialOptions);
this.externalComponent = externalComponent;
this.componentCreator = componentCreator;
this.reactInstanceManager = reactInstanceManager;
this.emitter = emitter;
}

@Override
protected ExternalComponentLayout createView() {
ExternalComponentLayout content = new ExternalComponentLayout(getActivity());
content.addView(componentCreator.create(getActivity(), reactInstanceManager, externalComponent.passProps).asView());
content.addView(componentCreator
.create(getActivity(), reactInstanceManager, externalComponent.passProps)
.asView());
return content;
}

@Override
public void sendOnNavigationButtonPressed(String buttonId) {
ReactContext currentReactContext = reactInstanceManager.getCurrentReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).emitOnNavigationButtonPressed(getId(), buttonId);
}
emitter.emitOnNavigationButtonPressed(getId(), buttonId);
}

@Override
Expand All @@ -46,6 +46,18 @@ public void mergeOptions(Options options) {
super.mergeOptions(options);
}

@Override
public void onViewAppeared() {
super.onViewAppeared();
emitter.emitComponentDidAppear(getId(), externalComponent.name.get());
}

@Override
public void onViewDisappear() {
super.onViewDisappear();
emitter.emitComponentDidDisappear(getId(), externalComponent.name.get());
}

public FragmentActivity getActivity() {
return (FragmentActivity) super.getActivity();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.reactnativenavigation.parse.ExternalComponent;
import com.reactnativenavigation.parse.Options;
import com.reactnativenavigation.parse.params.Text;
import com.reactnativenavigation.react.EventEmitter;
import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentViewController;
import com.reactnativenavigation.viewcontrollers.externalcomponent.FragmentCreatorMock;
import com.reactnativenavigation.views.ExternalComponentLayout;
Expand All @@ -28,29 +29,25 @@ public class ExternalComponentViewControllerTest extends BaseTest {
private Activity activity;
private ExternalComponent ec;
private ReactInstanceManager reactInstanceManager;
private EventEmitter emitter;

@Override
public void beforeEach() {
componentCreator = spy(new FragmentCreatorMock());
activity = newActivity();
ec = createExternalComponent();
reactInstanceManager = Mockito.mock(ReactInstanceManager.class);
emitter = Mockito.mock(EventEmitter.class);
uut = spy(new ExternalComponentViewController(activity,
"fragmentId",
ec,
componentCreator,
reactInstanceManager,
emitter,
new Options())
);
}

private ExternalComponent createExternalComponent() {
ExternalComponent component = new ExternalComponent();
component.name = new Text("fragmentComponent");
component.passProps = new JSONObject();
return component;
}

@Test
public void createView_returnsFrameLayout() {
ExternalComponentLayout view = uut.getView();
Expand All @@ -63,4 +60,23 @@ public void createView_createsExternalComponent() {
verify(componentCreator, times(1)).create((FragmentActivity) activity, reactInstanceManager, ec.passProps);
assertThat(view.getChildCount()).isGreaterThan(0);
}

@Test
public void onViewAppeared_appearEventIsEmitted() {
uut.onViewAppeared();
verify(emitter).emitComponentDidAppear(uut.getId(), ec.name.get());
}

@Test
public void onViewDisappear_disappearEventIsEmitted() {
uut.onViewDisappear();
verify(emitter).emitComponentDidDisappear(uut.getId(), ec.name.get());
}

private ExternalComponent createExternalComponent() {
ExternalComponent component = new ExternalComponent();
component.name = new Text("fragmentComponent");
component.passProps = new JSONObject();
return component;
}
}
55 changes: 55 additions & 0 deletions playground/src/screens/ExternalComponentScreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const React = require('react');
const Root = require('../components/Root');
const Button = require('../components/Button');
const Screens = require('./Screens');
const Navigation = require('../services/Navigation');
const { stack } = require('../commons/Layouts');
const {
PUSH_BTN,
MODAL_BTN
} = require('../testIDs');

class ExternalComponentScreen extends React.Component {
static options() {
return {
topBar: {
title: {
text: 'External Component'
}
}
}
}

render() {
return (
<Root componentId={this.props.componentId}>
<Button label='Push' testID={PUSH_BTN} onPress={this.push} />
<Button label='Show Modal' testID={MODAL_BTN} onPress={this.modal} />
</Root>
);
}

push = () => Navigation.push(this, {
externalComponent: {
name: Screens.NativeScreen,
passProps: {
text: 'This is an external component'
}
}
});
modal = () => Navigation.showModal(
stack([
Screens.Pushed,
{
externalComponent: {
name: Screens.NativeScreen,
passProps: {
text: 'External component in deep stack'
}
}
}
])
);
}

module.exports = ExternalComponentScreen;
3 changes: 2 additions & 1 deletion playground/src/screens/NavigationScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const {
OVERLAY_BTN,
EXTERNAL_COMP_BTN,
SHOW_STATIC_EVENTS_SCREEN,
SHOW_ORIENTATION_SCREEN
SHOW_ORIENTATION_SCREEN,
TOP_BAR_ELEMENT
} = require('../testIDs');
const Screens = require('./Screens');

Expand Down
1 change: 1 addition & 0 deletions playground/src/screens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function registerScreens() {
Navigation.registerComponent(Screens.Orientation, () => require('./OrientationScreen'));
Navigation.registerComponent(Screens.OrientationDetect, () => require('./OrientationDetectScreen'));
Navigation.registerComponent(Screens.Search, () => require('./SearchScreen'));
Navigation.registerComponent(Screens.ExternalComponent, () => require('./ExternalComponentScreen'));

Navigation.registerComponent(`navigation.playground.CustomTransitionDestination`, () => CustomTransitionDestination);
Navigation.registerComponent(`navigation.playground.CustomTransitionOrigin`, () => CustomTransitionOrigin);
Expand Down

0 comments on commit 602c669

Please sign in to comment.