From 3b357328004c1e8283a8dbac931aad959afdb89e Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 25 Jul 2016 03:34:06 -0700 Subject: [PATCH] Add Share module Summary: revision of https://github.com/facebook/react-native/pull/5476 It has only one method `shareTextContent` and next will be`shareBinaryContent`. In Android, Promise can't receive a result, because `startActivityForResult` is not working with `Intent.ACTION_SEND`. Maybe we can use `createChooser(Intent target, CharSequence title, IntentSender sender)` which requires API level 22. Closes https://github.com/facebook/react-native/pull/5904 Differential Revision: D3612889 fbshipit-source-id: 0e7aaf34b076a99089cc76bd649e6da067d9a760 --- Examples/UIExplorer/js/ShareExample.js | 124 ++++++++++++ .../UIExplorer/js/UIExplorerList.android.js | 4 + Examples/UIExplorer/js/UIExplorerList.ios.js | 4 + Libraries/Share/Share.js | 116 ++++++++++++ Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + .../java/com/facebook/react/tests/BUCK | 1 + .../facebook/react/tests/ShareTestCase.java | 109 +++++++++++ .../src/androidTest/js/ShareTestModule.js | 41 ++++ ReactAndroid/src/androidTest/js/TestBundle.js | 5 + .../com/facebook/react/modules/share/BUCK | 20 ++ .../react/modules/share/ShareModule.java | 90 +++++++++ .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 2 + .../test/java/com/facebook/react/modules/BUCK | 2 + .../react/modules/share/ShareModuleTest.java | 179 ++++++++++++++++++ 16 files changed, 700 insertions(+) create mode 100644 Examples/UIExplorer/js/ShareExample.js create mode 100644 Libraries/Share/Share.js create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java create mode 100644 ReactAndroid/src/androidTest/js/ShareTestModule.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java diff --git a/Examples/UIExplorer/js/ShareExample.js b/Examples/UIExplorer/js/ShareExample.js new file mode 100644 index 000000000..4a2f2b84d --- /dev/null +++ b/Examples/UIExplorer/js/ShareExample.js @@ -0,0 +1,124 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + View, + Text, + TouchableHighlight, + Share, +} = ReactNative; + +exports.framework = 'React'; +exports.title = 'Share'; +exports.description = 'Share data with other Apps.'; +exports.examples = [{ + title: 'Share Text Content', + render() { + return ; + } +}]; + +class ShareMessageExample extends React.Component { + _shareMessage: Function; + _shareText: Function; + _showResult: Function; + state: any; + + constructor(props) { + super(props); + + this._shareMessage = this._shareMessage.bind(this); + this._shareText = this._shareText.bind(this); + this._showResult = this._showResult.bind(this); + + this.state = { + result: '' + }; + } + + render() { + return ( + + + + Click to share message + + + + + Click to share message, URL and title + + + {this.state.result} + + ); + } + + _shareMessage() { + Share.share({ + message: 'React Native | A framework for building native apps using React' + }) + .then(this._showResult) + .catch((error) => this.setState({result: 'error: ' + error.message})); + } + + _shareText() { + Share.share({ + message: 'A framework for building native apps using React', + url: 'http://facebook.github.io/react-native/', + title: 'React Native' + }, { + dialogTitle: 'Share React Native website', + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ], + tintColor: 'green' + }) + .then(this._showResult) + .catch((error) => this.setState({result: 'error: ' + error.message})); + } + + _showResult(result) { + if (result.action === Share.sharedAction) { + if (result.activityType) { + this.setState({result: 'shared with an activityType: ' + result.activityType}); + } else { + this.setState({result: 'shared'}); + } + } else if (result.action === Share.dismissedAction) { + this.setState({result: 'dismissed'}); + } + } + +} + + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); diff --git a/Examples/UIExplorer/js/UIExplorerList.android.js b/Examples/UIExplorer/js/UIExplorerList.android.js index 561408045..dcd33a82c 100644 --- a/Examples/UIExplorer/js/UIExplorerList.android.js +++ b/Examples/UIExplorer/js/UIExplorerList.android.js @@ -181,6 +181,10 @@ const APIExamples = [ key: 'PointerEventsExample', module: require('./PointerEventsExample'), }, + { + key: 'ShareExample', + module: require('./ShareExample'), + }, { key: 'TimePickerAndroidExample', module: require('./TimePickerAndroidExample'), diff --git a/Examples/UIExplorer/js/UIExplorerList.ios.js b/Examples/UIExplorer/js/UIExplorerList.ios.js index 81f26bae6..8875f58e7 100644 --- a/Examples/UIExplorer/js/UIExplorerList.ios.js +++ b/Examples/UIExplorer/js/UIExplorerList.ios.js @@ -247,6 +247,10 @@ const APIExamples: Array = [ key: 'RCTRootViewIOSExample', module: require('./RCTRootViewIOSExample'), }, + { + key: 'ShareExample', + module: require('./ShareExample'), + }, { key: 'SnapshotExample', module: require('./SnapshotExample'), diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js new file mode 100644 index 000000000..22839a079 --- /dev/null +++ b/Libraries/Share/Share.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Share + * @flow + */ +'use strict'; + +const Platform = require('Platform'); +const { + ActionSheetManager, + ShareModule +} = require('NativeModules'); +const invariant = require('fbjs/lib/invariant'); +const processColor = require('processColor'); + +type Content = { title?: string, message: string } | { title?: string, url: string }; +type Options = { dialogTitle?: string, excludeActivityTypes?: Array, tintColor?: string }; + +class Share { + + /** + * Open a dialog to share text content. + * + * In iOS, Returns a Promise which will be invoked an object containing `action`, `activityType`. + * If the user dismissed the dialog, the Promise will still be resolved with action being `Share.dismissedAction` + * and all the other keys being undefined. + * + * In Android, Returns a Promise which always be resolved with action being `Share.sharedAction`. + * + * ### Content + * + * - `message` - a message to share + * - `title` - title of the message + * + * #### iOS + * + * - `url` - an URL to share + * + * At least one of URL and message is required. + * + * ### Options + * + * #### iOS + * + * - `excludedActivityTypes` + * - `tintColor` + * + * #### Android + * + * - `dialogTitle` + * + */ + static share(content: Content, options: Options = {}): Promise { + invariant( + typeof content === 'object' && content !== null, + 'Content must a valid object' + ); + invariant( + typeof content.url === 'string' || typeof content.message === 'string', + 'At least one of URL and message is required' + ); + invariant( + typeof options === 'object' && options !== null, + 'Options must be a valid object' + ); + + if (Platform.OS === 'android') { + invariant( + !content.title || typeof content.title === 'string', + 'Invalid title: title should be a string.' + ); + return ShareModule.share(content, options.dialogTitle); + } else if (Platform.OS === 'ios') { + return new Promise((resolve, reject) => { + ActionSheetManager.showShareActionSheetWithOptions( + {...content, ...options, tintColor: processColor(options.tintColor)}, + (error) => reject(error), + (success, activityType) => { + if (success) { + resolve({ + 'action': 'sharedAction', + 'activityType': activityType + }); + } else { + resolve({ + 'action': 'dismissedAction' + }); + } + } + ); + }); + } else { + return Promise.reject(new Error('Unsupported platform')); + } + } + + /** + * The content was successfully shared. + */ + static get sharedAction() { return 'sharedAction'; } + + /** + * The dialog has been dismissed. + * @platform ios + */ + static get dismissedAction() { return 'dismissedAction'; } + +} + +module.exports = Share; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 4ce701b3c..4f1b5d1b9 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -101,6 +101,7 @@ const ReactNative = { get PixelRatio() { return require('PixelRatio'); }, get PushNotificationIOS() { return require('PushNotificationIOS'); }, get Settings() { return require('Settings'); }, + get Share() { return require('Share'); }, get StatusBarIOS() { return require('StatusBarIOS'); }, get StyleSheet() { return require('StyleSheet'); }, get Systrace() { return require('Systrace'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 930ec4e04..721eb98e3 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -113,6 +113,7 @@ var ReactNative = { PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), Settings: require('Settings'), + Share: require('Share'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), Systrace: require('Systrace'), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index b9ba12654..359577f87 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -14,6 +14,7 @@ deps = [ react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/modules/core:core'), react_native_target('java/com/facebook/react/modules/datepicker:datepicker'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), react_native_target('java/com/facebook/react/touch:touch'), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java new file mode 100644 index 000000000..13ef775f3 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Instrumentation.ActivityMonitor; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.support.v4.app.DialogFragment; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.share.ShareModule; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; + +/** + * Test case for {@link ShareModule}. + */ +public class ShareTestCase extends ReactAppInstrumentationTestCase { + + private static interface ShareTestModule extends JavaScriptModule { + public void showShareDialog(WritableMap content, WritableMap options); + } + + private static class ShareRecordingModule extends BaseJavaModule { + + private int mOpened = 0; + private int mErrors = 0; + + @Override + public String getName() { + return "ShareRecordingModule"; + } + + @ReactMethod + public void recordOpened() { + mOpened++; + } + + @ReactMethod + public void recordError() { + mErrors++; + } + + public int getOpened() { + return mOpened; + } + + public int getErrors() { + return mErrors; + } + + } + + final ShareRecordingModule mRecordingModule = new ShareRecordingModule(); + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + return super.createReactInstanceSpecForTest() + .addNativeModule(mRecordingModule) + .addJSModule(ShareTestModule.class); + } + + @Override + protected String getReactApplicationKeyUnderTest() { + return "ShareTestApp"; + } + + private ShareTestModule getTestModule() { + return getReactContext().getCatalystInstance().getJSModule(ShareTestModule.class); + } + + public void testShowBasicShareDialog() { + final WritableMap content = new WritableNativeMap(); + content.putString("message", "Hello, ReactNative!"); + final WritableMap options = new WritableNativeMap(); + + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CHOOSER); + intentFilter.addCategory(Intent.CATEGORY_DEFAULT); + ActivityMonitor monitor = getInstrumentation().addMonitor(intentFilter, null, true); + + getTestModule().showShareDialog(content, options); + + waitForBridgeAndUIIdle(); + getInstrumentation().waitForIdleSync(); + + assertEquals(1, monitor.getHits()); + assertEquals(1, mRecordingModule.getOpened()); + assertEquals(0, mRecordingModule.getErrors()); + + } + +} diff --git a/ReactAndroid/src/androidTest/js/ShareTestModule.js b/ReactAndroid/src/androidTest/js/ShareTestModule.js new file mode 100644 index 000000000..dbba70103 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/ShareTestModule.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ShareTestModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var React = require('React'); +var RecordingModule = require('NativeModules').ShareRecordingModule; +var Share = require('Share'); +var View = require('View'); + +var ShareTestApp = React.createClass({ + render: function() { + return (); + }, +}); + +var ShareTestModule = { + ShareTestApp: ShareTestApp, + showShareDialog: function(content, options) { + Share.share(content, options).then( + () => RecordingModule.recordOpened(), + ({code, message}) => RecordingModule.recordError() + ); + }, +}; + +BatchedBridge.registerCallableModule( + 'ShareTestModule', + ShareTestModule +); + +module.exports = ShareTestModule; diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index 960c485a3..f560068a3 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -25,6 +25,7 @@ require('DatePickerDialogTestModule'); require('MeasureLayoutTestModule'); require('PickerAndroidTestModule'); require('ScrollViewTestModule'); +require('ShareTestModule'); require('SwipeRefreshLayoutTestModule'); require('TextInputTestModule'); require('TimePickerDialogTestModule'); @@ -74,6 +75,10 @@ var apps = [ appKey: 'ScrollViewTestApp', component: () => require('ScrollViewTestModule').ScrollViewTestApp, }, +{ + appKey: 'ShareTestApp', + component: () => require('ShareTestModule').ShareTestApp, +}, { appKey: 'SubviewsClippingTestApp', component: () => require('SubviewsClippingTestModule').App, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK new file mode 100644 index 000000000..4e95826b0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'share', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':share', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java new file mode 100644 index 000000000..eee2a42ca --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.share; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.ReactConstants; + +/** + * Intent module. Launch other activities or open URLs. + */ +public class ShareModule extends ReactContextBaseJavaModule { + + /* package */ static final String ACTION_SHARED = "sharedAction"; + /* package */ static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; + /* package */ static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; + + public ShareModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "ShareModule"; + } + + /** + * Open a chooser dialog to send text content to other apps. + * + * Refer http://developer.android.com/intl/ko/training/sharing/send.html + * + * @param content the data to send + * @param dialogTitle the title of the chooser dialog + */ + @ReactMethod + public void share(ReadableMap content, String dialogTitle, Promise promise) { + if (content == null) { + promise.reject(ERROR_INVALID_CONTENT, "Content cannot be null"); + return; + } + + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setTypeAndNormalize("text/plain"); + + if (content.hasKey("title")) { + intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("title")); + } + + if (content.hasKey("message")) { + intent.putExtra(Intent.EXTRA_TEXT, content.getString("message")); + } + + Intent chooser = Intent.createChooser(intent, dialogTitle); + chooser.addCategory(Intent.CATEGORY_DEFAULT); + + Activity currentActivity = getCurrentActivity(); + if (currentActivity != null) { + currentActivity.startActivity(chooser); + } else { + getReactApplicationContext().startActivity(chooser); + } + WritableMap result = Arguments.createMap(); + result.putString("action", ACTION_SHARED); + promise.resolve(result); + + } catch (Exception e) { + promise.reject(ERROR_UNABLE_TO_OPEN_DIALOG, "Failed to open share dialog"); + } + + } + +} \ No newline at end of file diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index ea7e3322b..5acaa4342 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -27,6 +27,7 @@ android_library( react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), react_native_target('java/com/facebook/react/modules/network:network'), react_native_target('java/com/facebook/react/modules/permissions:permissions'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/statusbar:statusbar'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index ab46c7231..878247c0d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -33,6 +33,7 @@ import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.permissions.PermissionsModule; +import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.timepicker.TimePickerDialogModule; @@ -89,6 +90,7 @@ public class MainReactPackage implements ReactPackage { new NetworkingModule(reactContext), new NetInfoModule(reactContext), new PermissionsModule(reactContext), + new ShareModule(reactContext), new StatusBarModule(reactContext), new TimePickerDialogModule(reactContext), new ToastModule(reactContext), diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index be5828052..33400cb7a 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -9,6 +9,7 @@ robolectric3_test( CSSLAYOUT_TARGET, react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), react_native_dep('third-party/java/junit:junit'), react_native_dep('third-party/java/mockito:mockito'), react_native_dep('third-party/java/okhttp:okhttp3'), @@ -24,6 +25,7 @@ robolectric3_test( react_native_target('java/com/facebook/react/modules/debug:debug'), react_native_target('java/com/facebook/react/modules/dialog:dialog'), react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_target('java/com/facebook/react/touch:touch'), diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java new file mode 100644 index 000000000..f9d1b31e7 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.share; + +import android.app.Activity; +import android.content.Intent; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.JavaOnlyMap; + +import javax.annotation.Nullable; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@PrepareForTest({Arguments.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ShareModuleTest { + + private Activity mActivity; + private ShareModule mShareModule; + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Before + public void prepareModules() throws Exception { + PowerMockito.mockStatic(Arguments.class); + Mockito.when(Arguments.createMap()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyMap(); + } + }); + + mShareModule = new ShareModule(ReactTestHelper.createCatalystContextForTest()); + } + + @After + public void cleanUp() { + mActivity = null; + mShareModule = null; + } + + @Test + public void testShareDialog() { + final String title = "Title"; + final String message = "Message"; + final String dialogTitle = "Dialog Title"; + + JavaOnlyMap content = new JavaOnlyMap(); + content.putString("title", title); + content.putString("message", message); + + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(content, dialogTitle, promise); + + final Intent chooserIntent = + ((ShadowApplication)ShadowExtractor.extract(RuntimeEnvironment.application)).getNextStartedActivity(); + assertNotNull("Dialog was not displayed", chooserIntent); + assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); + assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); + + final Intent contentIntent = (Intent)chooserIntent.getExtras().get(Intent.EXTRA_INTENT); + assertNotNull("Intent was not built correctly", contentIntent); + assertEquals(Intent.ACTION_SEND, contentIntent.getAction()); + assertEquals(title, contentIntent.getExtras().get(Intent.EXTRA_SUBJECT)); + assertEquals(message, contentIntent.getExtras().get(Intent.EXTRA_TEXT)); + + assertEquals(1, promise.getResolved()); + } + + @Test + public void testInvalidContent() { + final String dialogTitle = "Dialog Title"; + + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(null, dialogTitle, promise); + + assertEquals(1, promise.getRejected()); + assertEquals(ShareModule.ERROR_INVALID_CONTENT, promise.getErrorCode()); + } + + final static class SimplePromise implements Promise { + private static final String DEFAULT_ERROR = "EUNSPECIFIED"; + + private int mResolved; + private int mRejected; + private Object mValue; + private String mErrorCode; + private String mErrorMessage; + + public int getResolved() { + return mResolved; + } + + public int getRejected() { + return mRejected; + } + + public Object getValue() { + return mValue; + } + + public String getErrorCode() { + return mErrorCode; + } + + public String getErrorMessage() { + return mErrorMessage; + } + + @Override + public void resolve(Object value) { + mResolved++; + mValue = value; + } + + @Override + public void reject(String code, String message) { + reject(code, message, /*Throwable*/null); + } + + @Override + @Deprecated + public void reject(String message) { + reject(DEFAULT_ERROR, message, /*Throwable*/null); + } + + @Override + public void reject(String code, Throwable e) { + reject(code, e.getMessage(), e); + } + + @Override + public void reject(Throwable e) { + reject(DEFAULT_ERROR, e.getMessage(), e); + } + + @Override + public void reject(String code, String message, @Nullable Throwable e) { + mRejected++; + mErrorCode = code; + mErrorMessage = message; + } + } + +}