Skip to content

Commit 950ff8c

Browse files
authored
Android unit test (#4372)
* Add android unit test * Add checks * Add missing update gradle version * Use polling to wait for specific view to be displayed/visible
1 parent 998f23d commit 950ff8c

File tree

17 files changed

+542
-122
lines changed

17 files changed

+542
-122
lines changed

pjsip-apps/src/swig/java/android/app-kotlin/build.gradle

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ plugins {
66
android {
77
compileSdk 33
88

9+
buildFeatures.buildConfig true
10+
911
defaultConfig {
1012
applicationId "org.pjsip.pjsua2.app_kotlin"
1113
minSdkVersion 23
@@ -20,6 +22,18 @@ android {
2022
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
2123
}
2224
}
25+
flavorDimensions "version"
26+
productFlavors {
27+
demo {
28+
isDefault true
29+
dimension "version"
30+
buildConfigField "int", "SIP_PORT", "6000"
31+
}
32+
ciTest {
33+
dimension "version"
34+
buildConfigField "int", "SIP_PORT", "6677"
35+
}
36+
}
2337
compileOptions {
2438
sourceCompatibility JavaVersion.VERSION_1_8
2539
targetCompatibility JavaVersion.VERSION_1_8
@@ -33,7 +47,7 @@ android {
3347
dependencies {
3448

3549
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
36-
implementation 'com.android.support:appcompat-v7:28.0.0'
37-
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
50+
implementation 'androidx.appcompat:appcompat:1.6.1'
51+
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
3852
implementation project(path: ':pjsua2')
3953
}

pjsip-apps/src/swig/java/android/app-kotlin/src/main/java/org/pjsip/pjsua2/app_kotlin/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import android.hardware.camera2.CameraManager
55
import android.os.Bundle
66
import android.os.Handler
77
import android.os.Message
8-
import android.support.v4.app.ActivityCompat
9-
import android.support.v7.app.AppCompatActivity
8+
import androidx.core.app.ActivityCompat
9+
import androidx.appcompat.app.AppCompatActivity;
1010
import android.view.SurfaceHolder
1111
import android.view.SurfaceView
1212
import android.view.View

pjsip-apps/src/swig/java/android/app-kotlin/src/main/res/layout/activity_main.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
2+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:app="http://schemas.android.com/apk/res-auto"
44
xmlns:tools="http://schemas.android.com/tools"
55
android:id="@+id/relativeLayout"
@@ -81,4 +81,4 @@
8181
</RelativeLayout>
8282

8383

84-
</android.support.constraint.ConstraintLayout>
84+
</androidx.constraintlayout.widget.ConstraintLayout>

pjsip-apps/src/swig/java/android/app/build.gradle

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ apply plugin: 'com.android.application'
22

33
android {
44
compileSdk 33
5+
buildFeatures.buildConfig true
6+
57

68
defaultConfig {
79
applicationId "org.pjsip.pjsua2.app"
810
minSdkVersion 23
911
targetSdkVersion 33
12+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1013
}
1114

1215
buildTypes {
@@ -15,10 +18,34 @@ android {
1518
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
1619
}
1720
}
21+
flavorDimensions "version"
22+
productFlavors {
23+
demo {
24+
isDefault true
25+
dimension "version"
26+
buildConfigField "int", "SIP_PORT", "6000"
27+
buildConfigField "boolean", "IS_TEST", "false"
28+
buildConfigField "String", "TEST_TAG", "\"demo\""
29+
}
30+
ciTest {
31+
dimension "version"
32+
buildConfigField "int", "SIP_PORT", "6677"
33+
buildConfigField "boolean", "IS_TEST", "true"
34+
buildConfigField "String", "TEST_TAG", "\"ciTest\""
35+
}
36+
}
1837
namespace 'org.pjsip.pjsua2.app'
1938
}
2039

2140
dependencies {
2241
implementation project(path: ':pjsua2')
23-
implementation 'com.android.support:appcompat-v7:28.0.0'
42+
implementation 'androidx.appcompat:appcompat:1.6.1'
43+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
44+
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
45+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
46+
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
47+
androidTestImplementation 'androidx.test:runner:1.5.2'
48+
androidTestImplementation 'androidx.test:rules:1.5.0'
49+
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
50+
testImplementation 'junit:junit:4.12'
2451
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<instrumentation
5+
android:name="androidx.test.runner.AndroidJUnitRunner"
6+
android:targetPackage="org.pjsip.pjsua2.app"
7+
android:handleProfiling="false"
8+
android:functionalTest="false" />
9+
10+
<application>
11+
<uses-library android:name="android.test.runner" />
12+
</application>
13+
14+
</manifest>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright (C) 2025 Teluu Inc. (http://www.teluu.com)
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17+
*/
18+
package org.pjsip.pjsua2.app;
19+
20+
import org.junit.Assert;
21+
import org.pjsip.pjsua2.*;
22+
import org.pjsip.pjsua2.app.BuildConfig;
23+
24+
import java.io.File;
25+
import java.util.HashMap;
26+
27+
import android.Manifest;
28+
import android.content.Context;
29+
import android.os.Environment;
30+
import android.util.Log;
31+
import android.util.SparseBooleanArray;
32+
import android.widget.ListView;
33+
34+
import androidx.test.espresso.IdlingRegistry;
35+
import androidx.test.espresso.IdlingResource;
36+
import androidx.test.espresso.NoMatchingViewException;
37+
import androidx.test.ext.junit.rules.ActivityScenarioRule;
38+
import androidx.test.ext.junit.runners.AndroidJUnit4;
39+
import androidx.test.platform.app.InstrumentationRegistry;
40+
import androidx.test.uiautomator.UiDevice;
41+
42+
import static androidx.test.espresso.action.ViewActions.click;
43+
import static androidx.test.espresso.action.ViewActions.replaceText;
44+
import static androidx.test.espresso.assertion.ViewAssertions.matches;
45+
import static androidx.test.espresso.Espresso.onData;
46+
import static androidx.test.espresso.Espresso.onView;
47+
import static androidx.test.espresso.matcher.ViewMatchers.withId;
48+
import static androidx.test.espresso.matcher.ViewMatchers.withText;
49+
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
50+
import static androidx.test.espresso.matcher.ViewMatchers.Visibility;
51+
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
52+
import static org.hamcrest.Matchers.anything;
53+
54+
import org.junit.After;
55+
import org.junit.Before;
56+
import org.junit.Rule;
57+
import org.junit.Test;
58+
import org.junit.runner.RunWith;
59+
import static org.junit.Assert.*;
60+
61+
class Holder<T> {
62+
T value;
63+
}
64+
65+
@RunWith(AndroidJUnit4.class)
66+
public class Pjsua2Test {
67+
final static String TAG = "PJSUA2Test";
68+
69+
private UiDevice device;
70+
71+
@Rule
72+
public ActivityScenarioRule<MainActivity> activityScenarioRule =
73+
new ActivityScenarioRule<>(MainActivity.class);
74+
75+
@Before
76+
public void setup() {
77+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
78+
79+
InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
80+
InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(),
81+
"android.permission.CAMERA"
82+
);
83+
InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
84+
InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(),
85+
"android.permission.RECORD_AUDIO"
86+
);
87+
88+
Context appContext =
89+
InstrumentationRegistry.getInstrumentation().getTargetContext();
90+
assertEquals(BuildConfig.APPLICATION_ID, appContext.getPackageName());
91+
}
92+
93+
private static void checkIsDisplayed(final int viewId, final String viewName) {
94+
try {
95+
onView(withId(viewId)).check(matches(isDisplayed()));
96+
} catch (NoMatchingViewException e) {
97+
Assert.fail("View with ID R.id." + viewName + " was not found");
98+
}
99+
}
100+
101+
public static void waitForView(final int viewId,
102+
final long timeoutInMillis,
103+
final boolean isDisplayed) throws Exception
104+
{
105+
long startTime = System.currentTimeMillis();
106+
long endTime = startTime + timeoutInMillis;
107+
108+
while (System.currentTimeMillis() < endTime) {
109+
try {
110+
if (isDisplayed) {
111+
onView(withId(viewId)).check(matches(isDisplayed()));
112+
} else {
113+
onView(withId(viewId)).check(matches(
114+
withEffectiveVisibility(Visibility.VISIBLE)));
115+
}
116+
return;
117+
} catch (NoMatchingViewException | AssertionError e) {
118+
Thread.sleep(100);
119+
}
120+
}
121+
122+
throw new Exception("View with ID " + viewId + " not visible within " +
123+
timeoutInMillis + " milliseconds");
124+
}
125+
private void takeScreenshot(final String fileName) throws Exception {
126+
UiDevice device = UiDevice.getInstance(
127+
InstrumentationRegistry.getInstrumentation());
128+
File path = new File(Environment.getExternalStoragePublicDirectory(
129+
Environment.DIRECTORY_PICTURES).getAbsolutePath(),
130+
"screenshots");
131+
if (!path.exists()) {
132+
if (!path.mkdirs()) {
133+
System.err.println("Failed to create "+ path.getAbsolutePath() +
134+
" directory");
135+
return;
136+
}
137+
}
138+
System.out.println("Taking screenshot to : " + path.getAbsolutePath() +
139+
"//" + fileName);
140+
device.takeScreenshot(new File(path, fileName));
141+
}
142+
143+
@Test
144+
public void addBuddy() throws Exception{
145+
String localUri = "sip:localhost";
146+
147+
localUri += ":" + Integer.toString(BuildConfig.SIP_PORT);
148+
Log.d(TAG, "Starting addBuddy()");
149+
150+
checkIsDisplayed(R.id.buttonAddBuddy, "buttonAddBuddy");
151+
152+
activityScenarioRule.getScenario().onActivity(activity -> {
153+
activity.findViewById(R.id.buttonAddBuddy).setTag(BuildConfig.TEST_TAG);
154+
});
155+
onView(withId(R.id.buttonAddBuddy)).perform(click());
156+
157+
Log.d(TAG, "Wait for the dialog to be shown");
158+
waitForView(R.id.editTextUri, 3000, true);
159+
160+
Log.d(TAG, "Change the buddy URI");
161+
onView(withId(R.id.editTextUri)).perform(replaceText(localUri));
162+
163+
Log.d(TAG, "Click confirm");
164+
onView(withText("OK")).perform(click());
165+
Log.d(TAG, "Done addBuddy()");
166+
}
167+
@Test
168+
public void callBuddy() throws Exception{
169+
ListView listView;
170+
final Holder<ListView> lvHolder = new Holder<>();
171+
172+
Log.d(TAG, "Starting callBuddy()");
173+
174+
String localUri = "sip:localhost";
175+
localUri += ":" + Integer.toString(BuildConfig.SIP_PORT);
176+
177+
checkIsDisplayed(R.id.listViewBuddy, "listViewBuddy");
178+
179+
onData(anything())
180+
.inAdapterView(withId(R.id.listViewBuddy))
181+
.atPosition(0)
182+
.perform(click());
183+
184+
// Retrieve the ListView and the checked item position
185+
activityScenarioRule.getScenario().onActivity(activity -> {
186+
lvHolder.value = activity.findViewById(R.id.listViewBuddy);
187+
});
188+
listView = lvHolder.value;
189+
assert(listView != null);
190+
listView.setTag(BuildConfig.TEST_TAG);
191+
192+
int checkedPosition = listView.getCheckedItemPosition();
193+
SparseBooleanArray checkedItems = listView.getCheckedItemPositions();
194+
if (checkedItems != null) {
195+
for (int i=0; i<checkedItems.size(); i++) {
196+
if (checkedItems.valueAt(i)) {
197+
String item = listView.getAdapter().getItem(
198+
checkedItems.keyAt(i)). toString();
199+
}
200+
}
201+
}
202+
assertTrue("No item is checked",
203+
checkedPosition != ListView.INVALID_POSITION);
204+
205+
//Get the text of the checked item
206+
HashMap<String, String> checkedBuddy = (HashMap<String, String>)
207+
listView.getAdapter().getItem(checkedPosition);
208+
209+
String checkedURI = checkedBuddy.get("uri");
210+
211+
Log.d(TAG, "Selected URI is: " + checkedURI);
212+
assertEquals(localUri, checkedURI);
213+
214+
checkIsDisplayed(R.id.buttonCall, "buttonCall");
215+
activityScenarioRule.getScenario().onActivity(activity -> {
216+
activity.findViewById(R.id.buttonCall).setTag(BuildConfig.TEST_TAG);
217+
});
218+
onView(withId(R.id.buttonCall)).perform(click());
219+
220+
Log.d(TAG, "Wait for the incoming video to be shown");
221+
waitForView(R.id.surfaceIncomingVideo, 10000, false);
222+
223+
onView(withId(R.id.surfaceIncomingVideo))
224+
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
225+
try {
226+
takeScreenshot("make_call_sc.png");
227+
} catch (Exception e) {
228+
Log.e(TAG, e.getMessage());
229+
};
230+
Log.d(TAG, "Done callBuddy()");
231+
}
232+
233+
}

0 commit comments

Comments
 (0)