浏览代码

Detox android (#2811)

* Make detox android test suit pass

Disabled emulator animations due to limitation of espresso/detox which can't wait until
a push/pop animation ends

* Fix orientation parsing and enable detox orientation tests

* remove only

* Fix BottomTabs testId

* Support user orientation

* Fix rightButtons testId

* Add Modals tests

* e2e fix ios
Guy Carmeli 6 年前
父节点
当前提交
cb4ced860f
没有帐户链接到提交者的电子邮件

+ 3
- 3
AndroidE2E/app/src/androidTest/java/com/reactnativenavigation/e2e/androide2e/OverlayTest.java 查看文件

@@ -12,7 +12,7 @@ public class OverlayTest extends BaseTest {
12 12
 		elementByText("PUSH OPTIONS SCREEN").click();
13 13
         elementByText("SHOW OVERLAY").click();
14 14
 		assertExists(By.text("Test view"));
15
-        assetDismissed();
15
+        assertDismissed();
16 16
 	}
17 17
 
18 18
     @Test
@@ -22,10 +22,10 @@ public class OverlayTest extends BaseTest {
22 22
 		assertExists(By.text("Test view"));
23 23
         elementByText("DYNAMIC OPTIONS").click();
24 24
         assertExists(By.text("Dynamic Title"));
25
-        assetDismissed();
25
+        assertDismissed();
26 26
 	}
27 27
 
28
-    private void assetDismissed() throws UiObjectNotFoundException {
28
+    private void assertDismissed() throws UiObjectNotFoundException {
29 29
         elementByText("OK").click();
30 30
         assertExists(By.text("Overlay disappeared"));
31 31
         elementByText("OK").click();

+ 15
- 1
e2e/Modals.test.js 查看文件

@@ -1,7 +1,7 @@
1 1
 const Utils = require('./Utils');
2 2
 const testIDs = require('../playground/src/testIDs');
3 3
 
4
-const { elementByLabel, elementById } = Utils;
4
+const { elementByLabel, elementById, tapDeviceBackAndroid } = Utils;
5 5
 
6 6
 describe('modal', () => {
7 7
   beforeEach(async () => {
@@ -92,4 +92,18 @@ describe('modal', () => {
92 92
     await elementById(testIDs.DISMISS_ALL_MODALS_BUTTON).tap();
93 93
     await expect(elementById(testIDs.WELCOME_SCREEN_HEADER)).toBeVisible();
94 94
   });
95
+
96
+  it('push into modal', async () => {
97
+    await elementById(testIDs.SHOW_MODAL_BUTTON).tap();
98
+    await elementById(testIDs.PUSH_BUTTON).tap();
99
+    await expect(elementByLabel('Pushed Screen')).toBeVisible();
100
+  });
101
+
102
+  it(':android: push into modal', async () => {
103
+    await elementById(testIDs.SHOW_MODAL_BUTTON).tap();
104
+    await elementById(testIDs.PUSH_BUTTON).tap();
105
+    await elementById(testIDs.PUSH_BUTTON).tap();
106
+    tapDeviceBackAndroid();
107
+    await expect(elementByLabel('Pushed Screen')).toBeVisible();
108
+  });
95 109
 });

+ 4
- 3
e2e/Orientations.test.js 查看文件

@@ -4,7 +4,8 @@ const testIDs = require('../playground/src/testIDs');
4 4
 
5 5
 const { elementById } = Utils;
6 6
 
7
-describe(':ios: orientation', () => {
7
+describe('orientation', () => {
8
+
8 9
   beforeEach(async () => {
9 10
     await device.relaunchApp();
10 11
   });
@@ -31,7 +32,7 @@ describe(':ios: orientation', () => {
31 32
     await elementById(testIDs.DISMISS_BUTTON).tap();
32 33
   });
33 34
 
34
-  it('portrait only', async () => {
35
+  it(':ios: portrait only', async () => {
35 36
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
36 37
     await elementById(testIDs.PORTRAIT_ORIENTATION_BUTTON).tap();
37 38
     await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
@@ -42,7 +43,7 @@ describe(':ios: orientation', () => {
42 43
     await elementById(testIDs.DISMISS_BUTTON).tap();
43 44
   });
44 45
 
45
-  it('landscape only', async () => {
46
+  it(':ios: landscape only', async () => {
46 47
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
47 48
     await elementById(testIDs.LANDSCAPE_ORIENTATION_BUTTON).tap();
48 49
     await device.setOrientation('landscape');

+ 5
- 8
e2e/ScreenStyle.test.js 查看文件

@@ -21,7 +21,6 @@ describe('screen style', () => {
21 21
   });
22 22
 
23 23
   it('set dynamic options with valid options will do something and not crash', async () => {
24
-    // we have no way of testing individual styles for the screen
25 24
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
26 25
     await elementById(testIDs.DYNAMIC_OPTIONS_BUTTON).tap();
27 26
     await expect(elementById(testIDs.OPTIONS_SCREEN_HEADER)).toBeVisible();
@@ -35,14 +34,14 @@ describe('screen style', () => {
35 34
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
36 35
   });
37 36
 
38
-  it(':ios: hides topBar onScroll down and shows it on scroll up', async () => {
37
+  it('hides topBar onScroll down and shows it on scroll up', async () => {
39 38
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
40 39
     await elementById(testIDs.SCROLLVIEW_SCREEN_BUTTON).tap();
41 40
     await elementById(testIDs.TOGGLE_TOP_BAR_HIDE_ON_SCROLL).tap();
42 41
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
43 42
     await element(by.id(testIDs.SCROLLVIEW_ELEMENT)).swipe('up', 'slow');
44 43
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeNotVisible();
45
-    await element(by.id(testIDs.SCROLLVIEW_ELEMENT)).swipe('down', 'slow');
44
+    await element(by.id(testIDs.SCROLLVIEW_ELEMENT)).swipe('down', 'fast');
46 45
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
47 46
   });
48 47
 
@@ -52,14 +51,14 @@ describe('screen style', () => {
52 51
     await expect(element(by.text('TeSt'))).toBeVisible();
53 52
   });
54 53
 
55
-  it(':ios: hide Tab Bar', async () => {
54
+  it('hide Tab Bar', async () => {
56 55
     await elementById(testIDs.TAB_BASED_APP_BUTTON).tap();
57 56
     await expect(elementById(testIDs.BOTTOM_TABS_ELEMENT)).toBeVisible();
58 57
     await elementById(testIDs.HIDE_BOTTOM_TABS_BUTTON).tap();
59 58
     await expect(elementById(testIDs.BOTTOM_TABS_ELEMENT)).toBeNotVisible();
60 59
   });
61 60
 
62
-  it(':ios: show Tab Bar', async () => {
61
+  it('show Tab Bar', async () => {
63 62
     await elementById(testIDs.TAB_BASED_APP_BUTTON).tap();
64 63
     await elementById(testIDs.HIDE_BOTTOM_TABS_BUTTON).tap();
65 64
     await expect(elementById(testIDs.BOTTOM_TABS_ELEMENT)).toBeNotVisible();
@@ -83,7 +82,7 @@ describe('screen style', () => {
83 82
     await expect(elementById(testIDs.CENTERED_TEXT_HEADER)).toBeVisible();
84 83
   });
85 84
 
86
-  it(':ios: set right buttons', async () => {
85
+  it('set right buttons', async () => {
87 86
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
88 87
     await expect(elementById('buttonOne')).toBeVisible();
89 88
     await elementById('buttonOne').tap();
@@ -126,8 +125,6 @@ describe('screen style', () => {
126 125
 
127 126
   it('stack options should not override component options', async () => {
128 127
     await elementById(testIDs.TAB_BASED_APP_BUTTON).tap();
129
-    await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeNotVisible();
130
-    await elementById(testIDs.SECOND_TAB_BAR_BUTTON).tap();
131 128
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
132 129
   });
133 130
 });

+ 5
- 0
e2e/Utils.js 查看文件

@@ -1,3 +1,5 @@
1
+const exec = require('shell-utils').exec;
2
+
1 3
 module.exports = {
2 4
   elementByLabel: (label) => {
3 5
     return element(by.text(label));
@@ -11,5 +13,8 @@ module.exports = {
11 13
     } catch (err) {
12 14
       return element(by.type('_UIModernBarButton').and(by.label('Back'))).tap();
13 15
     }
16
+  },
17
+  tapDeviceBackAndroid: () => {
18
+    exec.execSync('adb shell input keyevent 4');
14 19
   }
15 20
 };

+ 9
- 0
e2e/init.js 查看文件

@@ -1,10 +1,19 @@
1 1
 const detox = require('detox');
2 2
 const config = require('../package.json').detox;
3
+const exec = require('shell-utils').exec;
3 4
 
4 5
 before(async () => {
5 6
   await detox.init(config, { launchApp: false });
7
+  disableAndroidEmulatorAnimations();
6 8
 });
7 9
 
8 10
 after(async () => {
9 11
   await detox.cleanup();
10 12
 });
13
+
14
+// Temporary solution, #2809
15
+function disableAndroidEmulatorAnimations() {
16
+  exec.execAsync(`adb shell settings put global window_animation_scale 0.0`);
17
+  exec.execAsync(`adb shell settings put global transition_animation_scale 0.0`);
18
+  exec.execAsync(`adb shell settings put global animator_duration_scale 0.0`);
19
+}

+ 3
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java 查看文件

@@ -64,6 +64,9 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
64 64
         if (other.backgroundColor.hasValue()) {
65 65
 		    backgroundColor = other.backgroundColor;
66 66
         }
67
+        if (other.testId.hasValue()) {
68
+            testId = other.testId;
69
+        }
67 70
     }
68 71
 
69 72
     void mergeWithDefault(final BottomTabsOptions defaultOptions) {

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/parse/Options.java 查看文件

@@ -19,7 +19,7 @@ public class Options implements DEFAULT_VALUES {
19 19
         Options result = new Options();
20 20
         if (json == null) return result;
21 21
 
22
-        result.orientationOptions = OrientationOptions.parse(json.optJSONArray("orientation"));
22
+        result.orientationOptions = OrientationOptions.parse(json);
23 23
         result.topBarOptions = TopBarOptions.parse(typefaceManager, json.optJSONObject("topBar"));
24 24
         result.topTabsOptions = TopTabsOptions.parse(json.optJSONObject("topTabs"));
25 25
         result.topTabOptions = TopTabOptions.parse(typefaceManager, json.optJSONObject("topTab"));

+ 26
- 15
lib/android/app/src/main/java/com/reactnativenavigation/parse/OrientationOptions.java 查看文件

@@ -3,26 +3,33 @@ package com.reactnativenavigation.parse;
3 3
 import com.reactnativenavigation.parse.params.Orientation;
4 4
 
5 5
 import org.json.JSONArray;
6
+import org.json.JSONObject;
6 7
 
7 8
 import java.util.ArrayList;
8 9
 import java.util.Arrays;
9 10
 import java.util.List;
10 11
 
11 12
 public class OrientationOptions {
12
-    Orientation[] orientations = new Orientation[0];
13
+    List<Orientation> orientations = new ArrayList<>();
13 14
 
14
-    public static OrientationOptions parse(JSONArray orientations) {
15
+    public static OrientationOptions parse(JSONObject json) {
15 16
         OrientationOptions options = new OrientationOptions();
16
-        if (orientations == null) return options;
17
+        if (json == null) return options;
17 18
 
18
-        List<Orientation> parsed = new ArrayList<>();
19
-        for (int i = 0; i < orientations.length(); i++) {
20
-            Orientation o = Orientation.fromString(orientations.optString(i, "default"));
21
-            if (o != null) {
22
-                parsed.add(o);
19
+        JSONArray orientations = json.optJSONArray("orientation");
20
+        if (orientations == null) {
21
+            String orientation = json.optString("orientation", Orientation.Default.name);
22
+            options.orientations.add(Orientation.fromString(orientation));
23
+        } else {
24
+            List<Orientation> parsed = new ArrayList<>();
25
+            for (int i = 0; i < orientations.length(); i++) {
26
+                Orientation o = Orientation.fromString(orientations.optString(i, "default"));
27
+                if (o != null) {
28
+                    parsed.add(o);
29
+                }
23 30
             }
31
+            options.orientations = parsed;
24 32
         }
25
-        options.orientations = parsed.toArray(new Orientation[0]);
26 33
 
27 34
         return options;
28 35
     }
@@ -30,11 +37,15 @@ public class OrientationOptions {
30 37
     public int getValue() {
31 38
         if (!hasValue()) return Orientation.Default.orientationCode;
32 39
 
33
-        int result = 0;
34
-        for (Orientation orientation : orientations) {
35
-            result |= orientation.orientationCode;
40
+        switch (orientations.get(0)) {
41
+            case Landscape:
42
+                return orientations.contains(Orientation.Portrait) ? Orientation.PortraitLandscape.orientationCode : Orientation.Landscape.orientationCode;
43
+            case Portrait:
44
+                return orientations.contains(Orientation.Landscape) ? Orientation.PortraitLandscape.orientationCode : Orientation.Portrait.orientationCode;
45
+            default:
46
+            case Default:
47
+                return Orientation.Default.orientationCode;
36 48
         }
37
-        return result;
38 49
     }
39 50
 
40 51
     public void mergeWith(OrientationOptions other) {
@@ -42,7 +53,7 @@ public class OrientationOptions {
42 53
     }
43 54
 
44 55
     private boolean hasValue() {
45
-        return orientations.length > 0;
56
+        return !orientations.isEmpty();
46 57
     }
47 58
 
48 59
     public void mergeWithDefault(OrientationOptions defaultOptions) {
@@ -51,6 +62,6 @@ public class OrientationOptions {
51 62
 
52 63
     @Override
53 64
     public String toString() {
54
-        return hasValue() ? Arrays.toString(orientations) : Orientation.Default.toString();
65
+        return hasValue() ? Arrays.toString(orientations.toArray(new Orientation[0])) : Orientation.Default.toString();
55 66
     }
56 67
 }

+ 2
- 1
lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Orientation.java 查看文件

@@ -7,7 +7,8 @@ import android.support.annotation.Nullable;
7 7
 public enum Orientation {
8 8
     Portrait("portrait", Configuration.ORIENTATION_PORTRAIT, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT),
9 9
     Landscape("landscape", Configuration.ORIENTATION_LANDSCAPE, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE),
10
-    Default("default", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
10
+    Default("default", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED),
11
+    PortraitLandscape("sensor", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_USER);
11 12
 
12 13
     public String name;
13 14
     public int configurationCode;

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/views/TitleBarButton.java 查看文件

@@ -158,7 +158,7 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
158 158
             ActionMenuView buttonsLayout = ViewUtils.findChildByClass(toolbar, ActionMenuView.class);
159 159
             List<TextView> buttons = ViewUtils.findChildrenByClass(buttonsLayout, TextView.class);
160 160
             for (TextView view : buttons) {
161
-                if (button.title.hasValue() && button.title.get().equals(view.getText())) {
161
+                if (button.title.hasValue() && button.title.get().equals(view.getText().toString())) {
162 162
                     view.setTag(testId.get());
163 163
                 } else if (button.icon.hasValue() && ArrayUtils.contains(view.getCompoundDrawables(), icon)) {
164 164
                     view.setTag(testId.get());

+ 32
- 6
lib/android/app/src/test/java/com/reactnativenavigation/parse/OrientationOptionsTest.java 查看文件

@@ -4,6 +4,8 @@ import com.reactnativenavigation.BaseTest;
4 4
 import com.reactnativenavigation.parse.params.Orientation;
5 5
 
6 6
 import org.json.JSONArray;
7
+import org.json.JSONException;
8
+import org.json.JSONObject;
7 9
 import org.junit.Test;
8 10
 
9 11
 import java.util.Arrays;
@@ -26,16 +28,34 @@ public class OrientationOptionsTest extends BaseTest {
26 28
     @Test
27 29
     public void parseOrientations() throws Exception {
28 30
         OrientationOptions options = OrientationOptions.parse(create("default", "landscape", "portrait"));
29
-        assertThat(options.orientations[0]).isEqualTo(Orientation.Default);
30
-        assertThat(options.orientations[1]).isEqualTo(Orientation.Landscape);
31
-        assertThat(options.orientations[2]).isEqualTo(Orientation.Portrait);
31
+        assertThat(options.orientations.get(0)).isEqualTo(Orientation.Default);
32
+        assertThat(options.orientations.get(1)).isEqualTo(Orientation.Landscape);
33
+        assertThat(options.orientations.get(2)).isEqualTo(Orientation.Portrait);
34
+    }
35
+
36
+    @Test
37
+    public void parseSingleOrientation() throws Exception {
38
+        OrientationOptions options = OrientationOptions.parse(create("landscape"));
39
+        assertThat(options.orientations.get(0)).isEqualTo(Orientation.Landscape);
40
+    }
41
+
42
+    @Test
43
+    public void landscapePortrait_regardedAsUserOrientation() throws Exception {
44
+        OrientationOptions options = OrientationOptions.parse(create("landscape", "portrait"));
45
+        assertThat(options.getValue()).isEqualTo(Orientation.PortraitLandscape.orientationCode);
46
+    }
47
+
48
+    @Test
49
+    public void portraitLandscape_regardedAsUserOrientation() throws Exception {
50
+        OrientationOptions options = OrientationOptions.parse(create("portrait", "landscape"));
51
+        assertThat(options.getValue()).isEqualTo(Orientation.PortraitLandscape.orientationCode);
32 52
     }
33 53
 
34 54
     @Test
35 55
     public void unsupportedOrientationsAreIgnored() throws Exception {
36 56
         OrientationOptions options = OrientationOptions.parse(create("default", "autoRotate"));
37 57
         assertThat(options.orientations).hasSize(1);
38
-        assertThat(options.orientations[0]).isEqualTo(Orientation.Default);
58
+        assertThat(options.orientations.get(0)).isEqualTo(Orientation.Default);
39 59
     }
40 60
 
41 61
     @Test
@@ -44,7 +64,13 @@ public class OrientationOptionsTest extends BaseTest {
44 64
         assertThat(options.getValue()).isEqualTo(Orientation.Default.orientationCode);
45 65
     }
46 66
 
47
-    private JSONArray create(String... orientations) {
48
-        return new JSONArray(Arrays.asList(orientations));
67
+    private JSONObject create(String... orientations) {
68
+        JSONObject orientation = new JSONObject();
69
+        try {
70
+            orientation.putOpt("orientation", orientations.length > 1 ? new JSONArray(Arrays.asList(orientations)) : orientations[0]);
71
+        } catch (JSONException e) {
72
+            throw new RuntimeException("Unable to create orientation object");
73
+        }
74
+        return orientation;
49 75
     }
50 76
 }

+ 18
- 5
lib/ios/RNNNavigationOptions.m 查看文件

@@ -41,11 +41,24 @@ const NSInteger TOP_BAR_TRANSPARENT_TAG = 78264803;
41 41
 	for (id key in otherOptions) {
42 42
 		if ([self hasProperty:key]) {
43 43
 			if ([[self valueForKey:key] isKindOfClass:[RNNOptions class]]) {
44
-			RNNOptions* options = [self valueForKey:key];
45
-			[options mergeWith:[otherOptions objectForKey:key]];
46
-		} else {
47
-			[self setValue:[otherOptions objectForKey:key] forKey:key];
48
-		} 		
44
+				RNNOptions* options = [self valueForKey:key];
45
+				[options mergeWith:[otherOptions objectForKey:key]];
46
+			} else {
47
+				[self setValue:[otherOptions objectForKey:key] forKey:key];
48
+			}
49
+		}
50
+	}
51
+}
52
+
53
+-(void)mergeIfEmptyWith:(NSDictionary *)otherOptions {
54
+	for (id key in otherOptions) {
55
+		if ([self hasProperty:key]) {
56
+			if ([[self valueForKey:key] isKindOfClass:[RNNOptions class]]) {
57
+				RNNOptions* options = [self valueForKey:key];
58
+				[options mergeIfEmptyWith:[otherOptions objectForKey:key]];
59
+			} else if (![self valueForKey:key]) {
60
+				[self setValue:[otherOptions objectForKey:key] forKey:key];
61
+			}
49 62
 		}
50 63
 	}
51 64
 }

+ 1
- 0
lib/ios/RNNOptions.h 查看文件

@@ -16,6 +16,7 @@
16 16
 
17 17
 - (instancetype)initWithDict:(NSDictionary*)dict;
18 18
 - (void)mergeWith:(NSDictionary*)otherOptions;
19
+- (void)mergeIfEmptyWith:(NSDictionary*)otherOptions;
19 20
 - (void)applyOn:(UIViewController *)viewController defaultOptions:(RNNOptions*)defaultOptions;
20 21
 - (BOOL)hasProperty:(NSString*)propName;
21 22
 

+ 8
- 0
lib/ios/RNNOptions.m 查看文件

@@ -22,6 +22,14 @@
22 22
 	}
23 23
 }
24 24
 
25
+-(void)mergeIfEmptyWith:(NSDictionary *)otherOptions {
26
+	for (id key in otherOptions) {
27
+		if ([self hasProperty:key] && ![self valueForKey:key]) {
28
+			[self setValue:[otherOptions objectForKey:key] forKey:key];
29
+		}
30
+	}
31
+}
32
+
25 33
 - (BOOL)hasProperty:(NSString*)propName {
26 34
 	return [self respondsToSelector:NSSelectorFromString(propName)];
27 35
 }

+ 1
- 1
lib/ios/RNNRootViewController.m 查看文件

@@ -69,7 +69,7 @@
69 69
 }
70 70
 
71 71
 - (void)mergeOptions:(NSDictionary *)options {
72
-	[self.options mergeWith:options];
72
+	[self.options mergeIfEmptyWith:options];
73 73
 }
74 74
 
75 75
 - (void)setCustomNavigationTitleView {

+ 4
- 1
playground/src/screens/WelcomeScreen.js 查看文件

@@ -69,7 +69,7 @@ class WelcomeScreen extends Component {
69 69
                     },
70 70
                     options: {
71 71
                       topBar: {
72
-                        visible: (Platform.OS === 'android') ? true : false,
72
+                        visible: true,
73 73
                         title: 'React Native Navigation!'
74 74
                       }
75 75
                     }
@@ -81,6 +81,9 @@ class WelcomeScreen extends Component {
81 81
                   title: 'Tab 1',
82 82
                   icon: require('../images/one.png'),
83 83
                   testID: testIDs.FIRST_TAB_BAR_BUTTON
84
+                },
85
+                topBar: {
86
+                  visible: false
84 87
                 }
85 88
               }
86 89
             }