Skip to content

Commit e1a4cd7

Browse files
authored
feat(Android): #8011 Convert disabled Android Unit Tests to Instrumentation Tests (#8041)
* Implemented first native android unitest * More Android Unitests * Converted TitleSubTitleLayoutTest * Enabled instrumentation and add run them on ATD * Added more tests * Fixed test application
1 parent 8cb9e6a commit e1a4cd7

34 files changed

+1583
-330
lines changed

lib/android/app/build.gradle

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ android {
3939
defaultConfig {
4040
minSdkVersion safeExtGetFallbackLowerBound('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
4141
targetSdkVersion safeExtGetFallbackLowerBound('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
42+
43+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
4244
}
4345
buildTypes {
4446
release {
@@ -53,6 +55,15 @@ android {
5355
}
5456

5557
testOptions {
58+
managedDevices {
59+
localDevices {
60+
pixel3aapi34 {
61+
device = "Pixel 3a"
62+
apiLevel = 34
63+
systemImageSource = "aosp-atd"
64+
}
65+
}
66+
}
5667
unitTests.includeAndroidResources = true
5768
unitTests.all { t ->
5869
maxHeapSize = "4g"
@@ -103,6 +114,7 @@ dependencies {
103114
//noinspection GradleDynamicVersion
104115
implementation 'com.facebook.react:react-native:+'
105116

117+
106118
if ("Playground".toLowerCase() == rootProject.name.toLowerCase()) {
107119
// tests only for our playground
108120
testImplementation 'junit:junit:4.13.2'
@@ -113,5 +125,22 @@ dependencies {
113125
testImplementation 'org.mockito:mockito-inline:4.6.1'
114126
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
115127
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion"
128+
129+
// Core testing libraries
130+
androidTestImplementation "androidx.test.ext:junit:1.2.1"
131+
androidTestImplementation 'org.assertj:assertj-core:3.11.1'
132+
133+
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
134+
135+
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.4"
136+
androidTestImplementation 'org.opentest4j:opentest4j:1.2.0'
137+
androidTestImplementation ("org.mockito.kotlin:mockito-kotlin:5.4.0") {
138+
exclude group: 'org.mockito', module: 'mockito-core'
139+
}
140+
141+
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
142+
143+
androidTestImplementation("com.facebook.react:hermes-android")
144+
116145
}
117146
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="com.reactnativenavigation.test">
3+
4+
<application
5+
android:name="com.reactnativenavigation.TestApplication">
6+
<activity
7+
android:name="com.reactnativenavigation.TestActivity"
8+
android:exported="true"
9+
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
10+
>
11+
<intent-filter>
12+
<action android:name="android.intent.action.MAIN" />
13+
<category android:name="android.intent.category.LAUNCHER" />
14+
</intent-filter>
15+
</activity>
16+
</application>
17+
</manifest>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.reactnativenavigation
2+
3+
abstract class BaseAndroidTest {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.reactnativenavigation
2+
3+
class TestActivity : NavigationActivity() {
4+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.reactnativenavigation
2+
3+
import com.facebook.react.ReactHost
4+
import com.facebook.react.ReactNativeHost
5+
import com.facebook.react.ReactPackage
6+
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
7+
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
8+
import com.facebook.react.shell.MainReactPackage
9+
import com.reactnativenavigation.react.NavigationPackage
10+
import com.reactnativenavigation.react.NavigationReactNativeHost
11+
12+
class TestApplication : NavigationApplication() {
13+
14+
15+
override val reactNativeHost: ReactNativeHost
16+
get() = object : NavigationReactNativeHost(this) {
17+
override fun getJSMainModuleName(): String {
18+
return "index"
19+
}
20+
21+
override fun getUseDeveloperSupport(): Boolean {
22+
return false
23+
}
24+
25+
public override fun getPackages(): List<ReactPackage> {
26+
val packages = listOf(MainReactPackage(null), NavigationPackage())
27+
return packages
28+
}
29+
30+
override val isHermesEnabled: Boolean
31+
get() = true
32+
33+
override val isNewArchEnabled: Boolean
34+
get() = true
35+
}
36+
37+
override val reactHost: ReactHost
38+
get() = getDefaultReactHost(this, reactNativeHost)
39+
40+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.reactnativenavigation;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.view.View;
6+
import android.view.ViewGroup;
7+
8+
import androidx.annotation.NonNull;
9+
10+
import com.reactnativenavigation.mocks.TitleBarReactViewCreatorMock;
11+
import com.reactnativenavigation.mocks.TopBarBackgroundViewCreatorMock;
12+
import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
13+
import com.reactnativenavigation.mocks.TypefaceLoaderMock;
14+
import com.reactnativenavigation.options.Options;
15+
import com.reactnativenavigation.options.params.Bool;
16+
import com.reactnativenavigation.utils.RenderChecker;
17+
import com.reactnativenavigation.viewcontrollers.stack.StackPresenter;
18+
import com.reactnativenavigation.react.events.EventEmitter;
19+
import com.reactnativenavigation.utils.CompatUtils;
20+
import com.reactnativenavigation.utils.ImageLoader;
21+
import com.reactnativenavigation.utils.UiUtils;
22+
import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry;
23+
import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController;
24+
import com.reactnativenavigation.viewcontrollers.stack.topbar.button.IconResolver;
25+
import com.reactnativenavigation.viewcontrollers.stack.StackControllerBuilder;
26+
import com.reactnativenavigation.viewcontrollers.stack.topbar.TopBarController;
27+
import com.reactnativenavigation.views.stack.StackLayout;
28+
import com.reactnativenavigation.views.stack.topbar.TopBar;
29+
30+
import org.mockito.Mockito;
31+
32+
public class TestUtils {
33+
public static StackControllerBuilder newStackController(Activity activity) {
34+
TopBarController topBarController = new TopBarController() {
35+
@Override
36+
protected TopBar createTopBar(@NonNull Context context, @NonNull StackLayout stackLayout) {
37+
TopBar topBar = super.createTopBar(context, stackLayout);
38+
topBar.layout(0, 0, 1000, UiUtils.getTopBarHeight(context));
39+
return topBar;
40+
}
41+
};
42+
return new StackControllerBuilder(activity, Mockito.mock(EventEmitter.class))
43+
.setId("stack" + CompatUtils.generateViewId())
44+
.setChildRegistry(new ChildControllersRegistry())
45+
.setTopBarController(topBarController)
46+
.setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(),
47+
new TopBarBackgroundViewCreatorMock(), new TitleBarButtonCreatorMock(),
48+
new IconResolver(activity, new ImageLoader()), new TypefaceLoaderMock(), new RenderChecker(),
49+
new Options()))
50+
.setInitialOptions(new Options());
51+
}
52+
53+
public static void hideBackButton(ViewController<?> viewController) {
54+
viewController.options.topBar.buttons.back.visible = new Bool(false);
55+
}
56+
57+
public static <T extends View> T spyOn(T child) {
58+
ViewGroup parent = (ViewGroup) child.getParent();
59+
int indexOf = parent.indexOfChild(child);
60+
parent.removeView(child);
61+
T spy = Mockito.spy(child);
62+
parent.addView(spy, indexOf);
63+
return spy;
64+
}
65+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.reactnativenavigation.fakes
2+
3+
import android.app.Activity
4+
import com.reactnativenavigation.mocks.ImageLoaderMock
5+
import com.reactnativenavigation.utils.ImageLoader
6+
import com.reactnativenavigation.viewcontrollers.stack.topbar.button.IconResolver
7+
8+
class IconResolverFake(activity: Activity, imageLoader: ImageLoader = ImageLoaderMock.mock()) : IconResolver(activity, imageLoader)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.reactnativenavigation.mocks;
2+
3+
import android.graphics.Canvas;
4+
import android.graphics.ColorFilter;
5+
import android.graphics.drawable.Drawable;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
10+
public class BackDrawable extends Drawable {
11+
@Override
12+
public void draw(@NonNull Canvas canvas) {
13+
14+
}
15+
16+
@Override
17+
public void setAlpha(int alpha) {
18+
19+
}
20+
21+
@Override
22+
public void setColorFilter(@Nullable ColorFilter colorFilter) {
23+
24+
}
25+
26+
@Override
27+
public int getOpacity() {
28+
return 0;
29+
}
30+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.reactnativenavigation.mocks
2+
3+
import android.graphics.Canvas
4+
import android.graphics.ColorFilter
5+
import android.graphics.drawable.Drawable
6+
7+
import com.reactnativenavigation.utils.ImageLoader
8+
import com.reactnativenavigation.utils.ImageLoader.ImagesLoadingListener
9+
import org.mockito.Mockito.doAnswer
10+
import org.mockito.kotlin.any
11+
import org.mockito.kotlin.mock
12+
import org.mockito.kotlin.whenever
13+
import java.util.*
14+
15+
object ImageLoaderMock {
16+
val mockDrawable: Drawable = object : Drawable() {
17+
override fun draw(canvas: Canvas) {}
18+
override fun setAlpha(alpha: Int) {}
19+
override fun setColorFilter(colorFilter: ColorFilter?) {}
20+
override fun getOpacity(): Int {
21+
return 0
22+
}
23+
}
24+
private val backIcon: Drawable = BackDrawable()
25+
26+
@JvmStatic
27+
fun mock(): ImageLoader {
28+
return this.mock(mockDrawable)
29+
}
30+
31+
@JvmStatic
32+
fun mock(returnDrawable: Drawable = mockDrawable): ImageLoader {
33+
val imageLoader = mock<ImageLoader>()
34+
doAnswer { invocation ->
35+
val urlCount = (invocation.arguments[1] as Collection<*>).size
36+
val drawables = Collections.nCopies(urlCount, returnDrawable)
37+
(invocation.arguments[2] as ImagesLoadingListener).onComplete(drawables)
38+
null
39+
}.`when`(imageLoader).loadIcons(any(), any(), any())
40+
41+
doAnswer { invocation ->
42+
(invocation.arguments[2] as ImagesLoadingListener).onComplete(returnDrawable)
43+
null
44+
}.`when`(imageLoader).loadIcon(any(), any(), any())
45+
46+
whenever(imageLoader.getBackButtonIcon(any())).thenReturn(backIcon)
47+
return imageLoader
48+
}
49+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.reactnativenavigation.mocks;
2+
3+
import static com.reactnativenavigation.utils.ObjectUtils.perform;
4+
5+
import android.app.Activity;
6+
import android.content.Context;
7+
import android.view.MotionEvent;
8+
9+
import androidx.annotation.NonNull;
10+
11+
import com.reactnativenavigation.options.Options;
12+
import com.reactnativenavigation.react.ReactView;
13+
import com.reactnativenavigation.viewcontrollers.child.ChildController;
14+
import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry;
15+
import com.reactnativenavigation.viewcontrollers.component.ComponentPresenterBase;
16+
import com.reactnativenavigation.viewcontrollers.viewcontroller.Presenter;
17+
import com.reactnativenavigation.viewcontrollers.viewcontroller.ScrollEventListener;
18+
import com.reactnativenavigation.views.component.ReactComponent;
19+
20+
public class SimpleViewController extends ChildController<SimpleViewController.SimpleView> {
21+
private final ComponentPresenterBase presenter = new ComponentPresenterBase();
22+
23+
public SimpleViewController(Activity activity, ChildControllersRegistry childRegistry, String id, Options options) {
24+
this(activity, childRegistry, id, new Presenter(activity, new Options()), options);
25+
}
26+
27+
public SimpleViewController(Activity activity, ChildControllersRegistry childRegistry, String id, Presenter presenter, Options options) {
28+
super(activity, childRegistry, id, presenter, options);
29+
}
30+
31+
@Override
32+
public SimpleView createView() {
33+
return new SimpleView(getActivity());
34+
}
35+
36+
@Override
37+
public void sendOnNavigationButtonPressed(String buttonId) {
38+
getView().sendOnNavigationButtonPressed(buttonId);
39+
}
40+
41+
@Override
42+
public void destroy() {
43+
if (!isDestroyed()) performOnParentController(parent -> parent.onChildDestroyed(this));
44+
super.destroy();
45+
}
46+
47+
@NonNull
48+
@Override
49+
public String toString() {
50+
return "SimpleViewController " + getId();
51+
}
52+
53+
@Override
54+
public int getTopInset() {
55+
int statusBarInset = resolveCurrentOptions().statusBar.isHiddenOrDrawBehind() ? 0 : 63;
56+
return statusBarInset + perform(getParentController(), 0, p -> p.getTopInset(this));
57+
}
58+
59+
@Override
60+
public void applyBottomInset() {
61+
if (view != null) presenter.applyBottomInset(view, getBottomInset());
62+
}
63+
64+
@Override
65+
public String getCurrentComponentName() {
66+
return null;
67+
}
68+
69+
public static class SimpleView extends ReactView implements ReactComponent {
70+
71+
public SimpleView(@NonNull Context context) {
72+
super(context, "compId", "compName");
73+
}
74+
75+
@Override
76+
public boolean isRendered() {
77+
return getChildCount() >= 1;
78+
}
79+
80+
@Override
81+
public boolean isReady() {
82+
return false;
83+
}
84+
85+
@Override
86+
public ReactView asView() {
87+
return this;
88+
}
89+
90+
@Override
91+
public void destroy() {
92+
93+
}
94+
95+
@Override
96+
public void sendOnNavigationButtonPressed(String buttonId) {
97+
98+
}
99+
100+
@Override
101+
public ScrollEventListener getScrollEventListener() {
102+
return null;
103+
}
104+
105+
@Override
106+
public void dispatchTouchEventToJs(MotionEvent event) {
107+
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)