Skip to content

Add Share module #5904

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

Closed
wants to merge 49 commits into from
Closed
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a76c6c0
Add Share module
deminoth Feb 13, 2016
424a665
change the way handling promise reject
deminoth Feb 13, 2016
5da16c7
buck file added
deminoth Feb 13, 2016
8c3bece
added buck target
deminoth Feb 13, 2016
1f5f0c0
add share module to flow file
deminoth Feb 13, 2016
fb5f6ea
Merge branch 'master' into share-module
deminoth Feb 13, 2016
f21d709
options handling fixed
deminoth Feb 13, 2016
8917788
Keyword "if" must be followed by whitespace.
deminoth Feb 14, 2016
767f914
Merge branch 'master' into share-module
deminoth Feb 15, 2016
de58c94
Merge branch 'master' into share-module
deminoth Apr 7, 2016
68804ca
refactoring
deminoth Apr 7, 2016
fb88c94
fix lint errors
deminoth Apr 7, 2016
9917041
Merge branch 'master' into share-module
deminoth Apr 7, 2016
72c6d50
improves platform check
deminoth Apr 8, 2016
2678aee
show chooser when could not get current activity
deminoth Apr 8, 2016
32847bb
logs when exception
deminoth Apr 8, 2016
0371750
fix CI errors
deminoth Apr 8, 2016
dfe1f63
change invariant module path
deminoth Apr 8, 2016
772523e
Merge branch 'share-module' of https://github.com/dobbit/react-native…
deminoth Apr 8, 2016
43209dc
update example (+3 squashed commits)
deminoth Apr 19, 2016
74e6966
uses legacy http library for robolectric 3.0 (see https://github.com/…
deminoth Apr 20, 2016
dc7fdaa
add a unit test for share module
deminoth Apr 20, 2016
7f39ae3
fix flow errors
deminoth Apr 20, 2016
08c29bd
Merge branch 'master' into share-module
deminoth Apr 20, 2016
375ee94
better invariant
deminoth Apr 26, 2016
851c4ea
Merge branch 'master' into share-module
deminoth May 2, 2016
4d6add8
indent fix
deminoth May 2, 2016
e398fd9
Fix up this pattern var React = require('react-native');
deminoth May 2, 2016
71a67fb
It's enough to use a mock Activity
deminoth May 2, 2016
4351eec
promise check and test case for call with an invalid content
deminoth May 2, 2016
92a793b
import missed
deminoth May 2, 2016
68ca817
replace `shadowOf` with `ShadowExtractor.extract`
deminoth May 2, 2016
d3e13ed
Merge branch 'master' into share-module
deminoth May 6, 2016
ae290f0
add jsr dep for @Nullable annotation
deminoth May 6, 2016
918aaf6
Merge branch 'master' into share-module
deminoth Jul 11, 2016
f4d7d3a
replace `Intent.createChooser` with custom share dialog
deminoth Jul 12, 2016
29b5e3b
android integration tests
deminoth Jul 13, 2016
db99ac4
lint fix
deminoth Jul 13, 2016
d0f6fb8
Block launching activity for next test cases
deminoth Jul 13, 2016
bc14456
back to `Intent.createChooser`
deminoth Jul 14, 2016
a404f49
dep for the `@Nullable` annotation
deminoth Jul 14, 2016
9c896e5
documentation update
deminoth Jul 14, 2016
de1ab8f
Merge branch 'master' into share-module
deminoth Jul 15, 2016
5127f30
move example file (#2f73ca8)
deminoth Jul 15, 2016
0834fc7
Merge branch 'master' into share-module
deminoth Jul 18, 2016
994cda0
Merge branch 'master' into share-module
deminoth Jul 22, 2016
2b52ff5
fix unit tests
deminoth Jul 22, 2016
dc1c57c
do not log the error if we're already rejecting
deminoth Jul 25, 2016
94023f9
`url` in `content` prop is iOS only now
deminoth Jul 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions Examples/UIExplorer/js/ShareExample.js
Original file line number Diff line number Diff line change
@@ -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 <ShareMessageExample />;
}
}];

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 (
<View>
<TouchableHighlight style={styles.wrapper}
onPress={this._shareMessage}>
<View style={styles.button}>
<Text>Click to share message</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={this._shareText}>
<View style={styles.button}>
<Text>Click to share message, URL and title</Text>
</View>
</TouchableHighlight>
<Text>{this.state.result}</Text>
</View>
);
}

_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,
},
});
4 changes: 4 additions & 0 deletions Examples/UIExplorer/js/UIExplorerList.android.js
Original file line number Diff line number Diff line change
@@ -181,6 +181,10 @@ const APIExamples = [
key: 'PointerEventsExample',
module: require('./PointerEventsExample'),
},
{
key: 'ShareExample',
module: require('./ShareExample'),
},
{
key: 'TimePickerAndroidExample',
module: require('./TimePickerAndroidExample'),
4 changes: 4 additions & 0 deletions Examples/UIExplorer/js/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
@@ -247,6 +247,10 @@ const APIExamples: Array<UIExplorerExample> = [
key: 'RCTRootViewIOSExample',
module: require('./RCTRootViewIOSExample'),
},
{
key: 'ShareExample',
module: require('./ShareExample'),
},
{
key: 'SnapshotExample',
module: require('./SnapshotExample'),
116 changes: 116 additions & 0 deletions Libraries/Share/Share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

* 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

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

* of patent rights can be found in the PATENTS file in the same directory.

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

*

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

* @providesModule Share

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

* @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<string>, 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

Choose a reason for hiding this comment

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

Potentially unsafe get/set usage Getters and setters with side effects are potentially unsafe and disabled by default. You may opt-in to using them anyway by putting unsafe.enable_getters_and_setters=true into the [options] section of your .flowconfig.

*
* #### 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<Object> {
invariant(
typeof content === 'object' && content !== null,
'Content must a valid object'
);

Choose a reason for hiding this comment

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

no-trailing-spaces: Trailing spaces not allowed.

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'
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this invariant should go under android specific code since iOS does not even accept such parameter.


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') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we skip this else if ? Should reduce the indentation. I am not sure if we need to warn about unsupported platform given we don't do it anywhere else (e.g. https://github.com/facebook/react-native/blob/master/Libraries/Linking/Linking.js#L123)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@grabbou this else if was a request of @andreicoman11. I guess it's for RN on Windows platform and furthermore.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah ok - then probably something to remember for other files :) thanks.

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'; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just have plain strings instead of constants.


/**
* The dialog has been dismissed.
* @platform ios
*/
static get dismissedAction() { return 'dismissedAction'; }

}

module.exports = Share;
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js
Original file line number Diff line number Diff line change
@@ -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'); },
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js.flow
Original file line number Diff line number Diff line change
@@ -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'),
Original file line number Diff line number Diff line change
@@ -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'),
Original file line number Diff line number Diff line change
@@ -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());

}

}
41 changes: 41 additions & 0 deletions ReactAndroid/src/androidTest/js/ShareTestModule.js
Original file line number Diff line number Diff line change
@@ -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 (<View />);
},
});

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;
5 changes: 5 additions & 0 deletions ReactAndroid/src/androidTest/js/TestBundle.js
Original file line number Diff line number Diff line change
@@ -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,
20 changes: 20 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK
Original file line number Diff line number Diff line change
@@ -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',
)
Original file line number Diff line number Diff line change
@@ -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");
}

}

}
1 change: 1 addition & 0 deletions ReactAndroid/src/main/java/com/facebook/react/shell/BUCK
Original file line number Diff line number Diff line change
@@ -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'),
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@
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 List<NativeModule> createNativeModules(ReactApplicationContext reactConte
new NetworkingModule(reactContext),
new NetInfoModule(reactContext),
new PermissionsModule(reactContext),
new ShareModule(reactContext),
new StatusBarModule(reactContext),
new TimePickerDialogModule(reactContext),
new ToastModule(reactContext),
2 changes: 2 additions & 0 deletions ReactAndroid/src/test/java/com/facebook/react/modules/BUCK
Original file line number Diff line number Diff line change
@@ -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'),
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice! Mind also adding a test that verifies we reject incorrect calls?


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<Object>() {
@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;
}
}

}