mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
incoming/outgoing call uitests
This commit is contained in:
parent
e0cc864a85
commit
7795bdd434
29 changed files with 1214 additions and 5 deletions
|
|
@ -20,13 +20,18 @@ job-android:
|
|||
- ./gradlew app:dependencies | grep org.linphone
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleRelease
|
||||
|
||||
after_script:
|
||||
- ln -s ./app/build/outputs/apk/debug/linphone-android-debug-*.apk ./apk/debug
|
||||
- ln -s ./app/build/outputs/apk/release/linphone-android-release-*.apk ./apk/release
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- ./app/build/outputs/apk/debug/linphone-android-debug-*.apk
|
||||
- ./app/build/outputs/apk/release/linphone-android-release-*.apk
|
||||
paths:
|
||||
- ./app/build
|
||||
- ./apk/debug
|
||||
- ./apk/release
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
expire_in: 2 hour
|
||||
|
||||
|
||||
.scheduled-job-android:
|
||||
|
|
|
|||
34
.gitlab-ci-files/job-uitests.yml
Normal file
34
.gitlab-ci-files/job-uitests.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#dependencies:
|
||||
#install 'Android SDK Command-line Tools' from Android Studio > Tools > SDK Manager > SDK Tools > Android SDK Command-line Tools
|
||||
|
||||
|
||||
variables:
|
||||
android_api: 33 #android 13
|
||||
emulator_type: apis #atd for api < 30 more efficient
|
||||
system_architecture: arm64-v8a #x86_64 or x86 for Intels
|
||||
android_system_image: system-images;android-$android_api;google_$emulator_type;$system_architecture
|
||||
emulator_device = pixel_7
|
||||
emulator_name: $emulator_device-api_$android_api-google_$emulator_type-arch_$system_architecture
|
||||
|
||||
job-android-uitests:
|
||||
|
||||
stage: uitests
|
||||
tags: [ "deploy" ]
|
||||
|
||||
dependencies:
|
||||
- job-android
|
||||
|
||||
before_script:
|
||||
- sdkmanager --install $android_system_image > emulatorSystemImageInstallation.log
|
||||
- echo no | ${AVDMANAGER_EXE} --verbose create avd --force --name $emulator_name --package $android_system_image --tag google_$emulator_type --abi $system_architecture --device emulator_device > emulatorCreation.log
|
||||
- abd start-server
|
||||
- emulator -avd $emulator_name """-no-window -no-audio"""&
|
||||
- ${WAIT_FOR_EMULATOR_EXE}
|
||||
- adb shell input keyevent 82
|
||||
|
||||
script:
|
||||
- ./gradlew -Pandroid.testInstrumentationRunnerArguments.class=org.linphone.call.OutgoingCallUITests -PscreportAutoClose=true connectedAndroidTest
|
||||
|
||||
after_script:
|
||||
- adb -s emulator-5554 emu kill
|
||||
- adb kill-server
|
||||
|
|
@ -19,3 +19,38 @@ stages:
|
|||
- build
|
||||
- uitests
|
||||
- deploy
|
||||
|
||||
|
||||
variables:
|
||||
PATH: /Users/quentin/Library/Android/sdk/emulator/:/Users/quentin/Library/Android/sdk/cmdline-tools/latest/bin/
|
||||
android_api: "33" #android 13
|
||||
emulator_type: apis #atd for api < 30 more efficient
|
||||
system_architecture: arm64-v8a #x86_64 or x86 for Intels
|
||||
android_system_image: system-images;android-$android_api;google_$emulator_type;$system_architecture
|
||||
emulator_device: pixel_7
|
||||
emulator_name: $emulator_device-api_$android_api-google_$emulator_type-arch_$system_architecture
|
||||
|
||||
|
||||
job-android-uitests:
|
||||
|
||||
stage: uitests
|
||||
tags: [ "android" ]
|
||||
|
||||
dependencies:
|
||||
- job-android
|
||||
|
||||
before_script:
|
||||
- echo $PATH
|
||||
- ${PATH}sdkmanager --install $android_system_image > emulatorSystemImageInstallation.log
|
||||
- echo no | ${PATH}avdmanager --verbose create avd --force --name $emulator_name --package $android_system_image --tag google_$emulator_type --abi $system_architecture --device emulator_device > emulatorCreation.log
|
||||
- abd start-server
|
||||
- ${PATH}emulator -avd $emulator_name &
|
||||
- wait-for-android-emulator
|
||||
- adb shell input keyevent 82
|
||||
|
||||
script:
|
||||
- ./gradlew -Pandroid.testInstrumentationRunnerArguments.class=org.linphone.call.OutgoingCallUITests -PscreportAutoClose=true connectedAndroidTest
|
||||
|
||||
after_script:
|
||||
- adb -s emulator-5554 emu kill
|
||||
- adb kill-server
|
||||
|
|
|
|||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "screport"]
|
||||
path = screport
|
||||
url = https://gitlab.linphone.org/BC/public/screport.git
|
||||
192
app/build.gradle
192
app/build.gradle
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-android-extensions'
|
||||
id 'kotlin-kapt'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
|
|
@ -86,6 +87,12 @@ android {
|
|||
versionCode appVersionCode
|
||||
versionName "${project.version}"
|
||||
applicationId getPackageName()
|
||||
|
||||
//testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
|
|
@ -188,7 +195,21 @@ android {
|
|||
dataBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
//sourceCompatibility JavaVersion.VERSION_1_8
|
||||
//targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
adbOptions {
|
||||
installOptions '-g', '-r'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
//jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
namespace 'org.linphone'
|
||||
testBuildType 'debug'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -202,6 +223,7 @@ dependencies {
|
|||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha05"
|
||||
implementation "androidx.window:window:1.0.0"
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.test:rules:1.4.0'
|
||||
|
||||
def nav_version = "2.5.3"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
|
|
@ -244,6 +266,15 @@ dependencies {
|
|||
|
||||
// Only enable leak canary prior to release
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// UITests dependencies
|
||||
debugImplementation 'androidx.test:runner:1.1.3'
|
||||
debugImplementation 'androidx.test:core:1.1.3'
|
||||
debugImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
||||
|
||||
debugImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
debugImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
|
||||
debugImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||
}
|
||||
|
||||
task generateContactsXml(type: Copy) {
|
||||
|
|
@ -272,3 +303,164 @@ if (crashlyticsAvailable) {
|
|||
packageReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
}
|
||||
}
|
||||
|
||||
// screenshots ui tests
|
||||
|
||||
def reportsDirectory = "$buildDir/reports/androidTests/connected"
|
||||
def screenshotDirectory = "$reportsDirectory/screenshots"
|
||||
|
||||
def embedScreenshotsTask = task('embedScreenshots', group: 'reporting') {
|
||||
doFirst {
|
||||
|
||||
def failureScreenshotsDirectory = new File(reportsDirectory, 'failures')
|
||||
|
||||
if (!failureScreenshotsDirectory.exists()) {
|
||||
println 'Could not find screenshot failures. Skipping...'
|
||||
return
|
||||
}
|
||||
|
||||
failureScreenshotsDirectory.eachFile { failedTestClassDirectory ->
|
||||
def failedTestClassName = failedTestClassDirectory.name
|
||||
|
||||
failedTestClassDirectory.eachFile { failedTestFile ->
|
||||
def failedTestName = failedTestFile.name
|
||||
|
||||
failedTestFile.eachFile { failedTestCase ->
|
||||
def failedTestCaseDescription = failedTestCase.name
|
||||
def pt1 = failedTestCaseDescription.indexOf(".")
|
||||
def line = failedTestCaseDescription.substring(0,pt1)
|
||||
def pt2 = failedTestCaseDescription.indexOf(".",pt1)
|
||||
def name = failedTestCaseDescription.substring(pt1,pt2)
|
||||
def pt3 = failedTestCaseDescription.indexOf(".",pt2)
|
||||
if (pt3 != -1) name += " (${failedTestCaseDescription.substring(pt3)})"
|
||||
|
||||
def failedTestClassJunitReportFile = new File(reportsDirectory, "${failedTestClassName}.html")
|
||||
|
||||
if (!failedTestClassJunitReportFile.exists()) {
|
||||
println "Could not find JUnit report file for test class '${failedTestClassJunitReportFile}'"
|
||||
return
|
||||
}
|
||||
|
||||
def failedTestJunitReportContent = failedTestClassJunitReportFile.text
|
||||
|
||||
def tab0 = "<a href=\"#tab0\">Tests</a>\n</li>\n</ul>\n<div id=\"tab0\" class=\"tab\">"
|
||||
def tab0Index = failedTestJunitReportContent.indexOf(tab0)
|
||||
|
||||
if (tab0Index != -1) {
|
||||
def newtab0 = "<a href=\"#tab0\">Failed tests</a>\n</li>\n<li>\n<a href=\"#tab1\">Tests</a>\n</li>\n</ul>\n<div id=\"tab0\" class=\"tab\">\n<h2>Failed tests</h2>\n</div>\n<div id=\"tab1\" class=\"tab\">"
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.replace(tab0,newtab0)
|
||||
}
|
||||
|
||||
def failedTest = "<h3 class=\"failures\">${failedTestName}</h3>"
|
||||
def failedTestIndex = failedTestJunitReportContent.indexOf(failedTest)
|
||||
def screenshotReport = ""
|
||||
|
||||
if (failedTestIndex == -1) {
|
||||
def newtestDiv = "<div class=\"test\">\n<a name=\"${failedTestName}\"></a>\n${failedTest}\n<span class=\"code\">\n<pre>\n</pre>\n</span>\n</div>"
|
||||
def endDiv = "</div>\n<div id=\"tab1\" class=\"tab\">"
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.replace(endDiv,newtestDiv+endDiv)
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.replace("<td>${failedTestName}</td>\n<td class=\"success\">passed","<td>${failedTestName}</td>\n<td class=\"failures\">failed")
|
||||
|
||||
def totalCounterStr = "id=\"tests\">\n<div class=\"counter\">"
|
||||
def totalCounterStart = failedTestJunitReportContent.indexOf(totalCounterStr) + totalCounterStr.length()
|
||||
def totalCounterEnd = failedTestJunitReportContent.indexOf("</div>",totalCounterStart)
|
||||
|
||||
def failureCounterStr = "id=\"failures\">\n<div class=\"counter\">"
|
||||
def failureCounterStart = failedTestJunitReportContent.indexOf(failureCounterStr,totalCounterEnd) + failureCounterStr.length()
|
||||
def failureCounterEnd = failedTestJunitReportContent.indexOf("</div>",failureCounterStart)
|
||||
|
||||
def percentInfoStr = "id=\"successRate\">\n<div class=\"percent\">"
|
||||
def percentInfoStart = failedTestJunitReportContent.indexOf(percentInfoStr,failureCounterEnd) + percentInfoStr.length()
|
||||
def percentInfoEnd = failedTestJunitReportContent.indexOf("%</div>",percentInfoStart)
|
||||
|
||||
def total = failedTestJunitReportContent.substring(totalCounterStart,totalCounterEnd).toInteger()
|
||||
def failure = failedTestJunitReportContent.substring(failureCounterStart,failureCounterEnd).toInteger()+1
|
||||
def percent = ((1 - failure/total)*100).toInteger()
|
||||
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.substring(0,failureCounterStart) + "$failure" + failedTestJunitReportContent.substring(failureCounterEnd)
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.substring(0,percentInfoStart) + "$percent" + failedTestJunitReportContent.substring(percentInfoEnd)
|
||||
|
||||
if (failedTestJunitReportContent.indexOf("\"infoBox success\" id=\"successRate\"") != -1) {
|
||||
failedTestJunitReportContent = failedTestJunitReportContent.replace("\"infoBox success\" id=\"successRate\"", "\"infoBox failures\" id=\"successRate\"")
|
||||
}
|
||||
|
||||
failedTestIndex = failedTestJunitReportContent.indexOf(failedTest)
|
||||
} else screenshotReport = "\n\n"
|
||||
|
||||
def imagesrc = "failures/${failedTestClassName}/${failedTestName}/${failedTestCase.name}"
|
||||
|
||||
screenshotReport += "display conficts detected with \"${name}\" line ${line}: (reference | capture | difference)\n"
|
||||
screenshotReport += "<div style=\"display: table\">\n"
|
||||
screenshotReport += "<img src=\"${imagesrc}/reference.png\" width =\"220\" style=\"padding: 5px\"/>\n"
|
||||
screenshotReport += "<img src=\"${imagesrc}/capture.png\" width =\"220\" style=\"padding: 5px\"/>\n"
|
||||
screenshotReport += "<img src=\"${imagesrc}/difference.png\" width =\"220\" style=\"padding: 5px\"/>\n"
|
||||
screenshotReport += "</div>"
|
||||
|
||||
def insertIndex = failedTestJunitReportContent.indexOf("</pre>",failedTestIndex)
|
||||
def firstPart = failedTestJunitReportContent.substring(0,insertIndex)
|
||||
def secondPart = failedTestJunitReportContent.substring(insertIndex)
|
||||
failedTestJunitReportContent = firstPart + screenshotReport + secondPart
|
||||
|
||||
failedTestClassJunitReportFile.write(failedTestJunitReportContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def clearScreenshotsTask = task('clearScreenshots', type: Exec) {
|
||||
executable "${android.getAdbExe().toString()}"
|
||||
args 'shell', 'rm', '-r', '/sdcard/Pictures/linphone_uitests'
|
||||
}
|
||||
|
||||
def fetchScreenshotsTask = task('fetchScreenshots', type: Exec, group: 'reporting') {
|
||||
executable "${android.getAdbExe().toString()}"
|
||||
args 'pull', '/sdcard/Pictures/linphone_uitests/.', screenshotDirectory
|
||||
|
||||
doFirst {
|
||||
new File(screenshotDirectory).mkdirs()
|
||||
}
|
||||
|
||||
finalizedBy {
|
||||
clearScreenshotsTask
|
||||
}
|
||||
}
|
||||
|
||||
def launchScreenshotsComparisonTask = task('launchScreenshotsComparison', type: Exec, group: 'reporting') {
|
||||
workingDir "$rootDir/screport/"
|
||||
def autoClose = "false"
|
||||
if (project.hasProperty("screportAutoClose") ) {
|
||||
autoClose = screportAutoClose
|
||||
}
|
||||
commandLine "python3", "launch.py", "-rp", "$projectDir/src/androidTest/java/org/linphone/screenshots", "-sp", "$projectDir/build/reports/androidTests/connected/screenshots", "-ac", "$autoClose"
|
||||
|
||||
dependsOn() {
|
||||
fetchScreenshotsTask
|
||||
}
|
||||
|
||||
finalizedBy {
|
||||
embedScreenshotsTask
|
||||
}
|
||||
}
|
||||
|
||||
def isScreenshotComparisonNeededTask = task('isScreenshotComparisonNeeded', type: Exec, group: 'reporting') {
|
||||
commandLine "${android.getAdbExe().toString()}", 'shell', 'cd', '/sdcard/Pictures/linphone_uitests', '||', 'echo false'
|
||||
standardOutput = new ByteArrayOutputStream()
|
||||
|
||||
doLast {
|
||||
launchScreenshotsComparisonTask.onlyIf {"$standardOutput" == ""}
|
||||
fetchScreenshotsTask.onlyIf {"$standardOutput" == ""}
|
||||
clearScreenshotsTask.onlyIf {"$standardOutput" == ""}
|
||||
embedScreenshotsTask.onlyIf {"$standardOutput" == ""}
|
||||
}
|
||||
|
||||
finalizedBy {
|
||||
launchScreenshotsComparisonTask
|
||||
}
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
connectedDebugAndroidTest.finalizedBy {
|
||||
isScreenshotComparisonNeededTask
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package org.linphone.call
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.linphone.methods.CallViewUITestsMethods
|
||||
import org.linphone.methods.UITestsUtils
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class IncomingCallUITests {
|
||||
|
||||
lateinit var methods: CallViewUITestsMethods
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
UITestsUtils.testAppSetup()
|
||||
methods = CallViewUITestsMethods()
|
||||
}
|
||||
|
||||
// notification tests
|
||||
@Test
|
||||
fun testDisplayCallPush() {
|
||||
methods.startIncomingCall()
|
||||
methods.endCall()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoAnswerCallPush() {
|
||||
methods.startIncomingCall()
|
||||
methods.noAnswerCallFromPush()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeclineCallPush() {
|
||||
methods.startIncomingCall()
|
||||
methods.declineCallFromPush()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnswerCallPush() {
|
||||
methods.startIncomingCall()
|
||||
methods.answerCallFromPush()
|
||||
methods.endCall()
|
||||
}
|
||||
|
||||
// incoming call view tests
|
||||
@Test
|
||||
fun testOpenIncomingCallView() {
|
||||
methods.startIncomingCall()
|
||||
methods.openIncomingCallViewFromPush()
|
||||
methods.endCall()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoAnswerIncomingCallView() {
|
||||
methods.startIncomingCall()
|
||||
methods.openIncomingCallViewFromPush()
|
||||
methods.noAnswerCallFromIncomingCall()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeclineIncomingCallView() {
|
||||
methods.startIncomingCall()
|
||||
methods.openIncomingCallViewFromPush()
|
||||
methods.declineCallFromIncomingCallView()
|
||||
methods.endCall()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAcceptIncomingCallView() {
|
||||
methods.startIncomingCall()
|
||||
methods.openIncomingCallViewFromPush()
|
||||
methods.answerCallFromIncomingCallView()
|
||||
methods.endCall()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package org.linphone.call
|
||||
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.linphone.R
|
||||
import org.linphone.methods.*
|
||||
import org.linphone.methods.UITestsScreenshots.takeScreenshot
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class OutgoingCallUITests {
|
||||
|
||||
lateinit var methods: CallViewUITestsMethods
|
||||
|
||||
@get:Rule
|
||||
val screenshotsRule = ScreenshotsRule(true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
UITestsUtils.testAppSetup()
|
||||
methods = CallViewUITestsMethods()
|
||||
takeScreenshot("dialer_view")
|
||||
methods.startOutgoingCall()
|
||||
takeScreenshot("outgoing_call_view")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
methods.endCall()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testViewDisplay() {
|
||||
methods.endCall()
|
||||
takeScreenshot("dialer_view", "declined")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoAnswer() {
|
||||
methods.noAnswerCallFromOutgoingCall()
|
||||
takeScreenshot("dialer_view", "no_answer")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleMute() {
|
||||
Espresso.onView(withId(R.id.microphone)).perform(ViewActions.click())
|
||||
takeScreenshot("outgoing_call_view", "mute")
|
||||
Espresso.onView(withId(R.id.microphone)).perform(ViewActions.click())
|
||||
takeScreenshot("outgoing_call_view")
|
||||
methods.endCall()
|
||||
takeScreenshot("dialer_view", "declined")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleSpeaker() {
|
||||
Espresso.onView(withId(R.id.speaker)).perform(ViewActions.click())
|
||||
takeScreenshot("outgoing_call_view", "speaker")
|
||||
Espresso.onView(withId(R.id.speaker)).perform(ViewActions.click())
|
||||
takeScreenshot("outgoing_call_view")
|
||||
methods.endCall()
|
||||
takeScreenshot("dialer_view", "declined")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCancel() {
|
||||
methods.cancelCallFromOutgoingCallView()
|
||||
takeScreenshot("dialer_view")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package org.linphone.methods
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AuthInfo
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.methods.UITestsUtils.activityScenario
|
||||
import org.linphone.methods.UITestsUtils.checkWithTimeout
|
||||
import org.linphone.utils.AppUtils.Companion.getString
|
||||
|
||||
class CallViewUITestsMethods {
|
||||
|
||||
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
val manager = UITestsCoreManager.instance
|
||||
val appAccountAuthInfo: AuthInfo = UITestsCoreManager.instance.appAccountAuthInfo
|
||||
val ghostAccount: UITestsRegisteredLinphoneCore = UITestsCoreManager.instance.ghostAccounts[0]
|
||||
|
||||
fun startIncomingCall() {
|
||||
if (ghostAccount.callState != Call.State.Released) { ghostAccount.terminateCall() }
|
||||
|
||||
ghostAccount.startCall(manager.createAddress(appAccountAuthInfo))
|
||||
ghostAccount.waitForCallState(Call.State.OutgoingRinging, 5.0)
|
||||
|
||||
waitForCallNotification(true, 5.0)
|
||||
}
|
||||
|
||||
fun startOutgoingCall() {
|
||||
if (ghostAccount.callState != Call.State.Released) { ghostAccount.terminateCall() }
|
||||
|
||||
onView(withId(R.id.sip_uri_input)).perform(typeText(ghostAccount.mAuthInfo.username))
|
||||
onView(withContentDescription(R.string.content_description_start_call)).perform(click())
|
||||
|
||||
onView(withId(R.id.outgoing_call_layout)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
}
|
||||
|
||||
fun endCall() {
|
||||
if (ghostAccount.callState == Call.State.Released) { return }
|
||||
|
||||
ghostAccount.terminateCall()
|
||||
ghostAccount.waitForCallState(Call.State.Released, 5.0)
|
||||
onView(withId(R.id.outgoing_call_layout)).checkWithTimeout(doesNotExist(), 5.0)
|
||||
// onView(withId(com.google.android.material.R.id.snackbar_text)).checkWithTimeout(doesNotExist(), 5.0)
|
||||
}
|
||||
|
||||
fun noAnswerCallFromPush() {
|
||||
waitForCallNotification(false, 30.0)
|
||||
}
|
||||
|
||||
fun declineCallFromPush() {
|
||||
val declineLabel = getString(R.string.incoming_call_notification_answer_action_label)
|
||||
|
||||
try {
|
||||
val decline = device.findObject(By.textContains(declineLabel))
|
||||
decline.click()
|
||||
} catch (e: java.lang.NullPointerException) {
|
||||
throw AssertionError("[UITests] Enable to find the \"$declineLabel\" button in the incoming call notification")
|
||||
}
|
||||
}
|
||||
|
||||
fun answerCallFromPush() {
|
||||
val answerLabel = getString(R.string.incoming_call_notification_answer_action_label)
|
||||
try {
|
||||
val answer = device.findObject(By.textContains(answerLabel))
|
||||
answer.click()
|
||||
} catch (e: java.lang.NullPointerException) {
|
||||
throw AssertionError("[UITests] Enable to find the \"$answerLabel\" button in the incoming call notification")
|
||||
}
|
||||
onView(withId(R.id.single_call_layout)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
}
|
||||
|
||||
fun openIncomingCallViewFromPush() {
|
||||
try {
|
||||
val notif = device.findObject(By.textContains(getString(R.string.incoming_call_notification_title)))
|
||||
notif.click()
|
||||
} catch (e: java.lang.NullPointerException) {
|
||||
throw AssertionError("[UITests] Enable to find the incoming call notification")
|
||||
}
|
||||
onView(withId(R.id.incoming_call_layout)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
}
|
||||
|
||||
fun declineCallFromIncomingCallView() {
|
||||
onView(withId(R.id.hangup)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
onView(withId(R.id.hangup)).perform(click())
|
||||
onView(withId(R.id.incoming_call_layout)).checkWithTimeout(doesNotExist(), 5.0)
|
||||
}
|
||||
|
||||
fun answerCallFromIncomingCallView() {
|
||||
onView(withId(R.id.answer)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
onView(withId(R.id.answer)).perform(click())
|
||||
onView(withId(R.id.single_call_layout)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
}
|
||||
|
||||
fun cancelCallFromOutgoingCallView() {
|
||||
onView(withId(R.id.hangup)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
onView(withId(R.id.hangup)).perform(click())
|
||||
onView(withId(R.id.outgoing_call_layout)).checkWithTimeout(doesNotExist(), 5.0)
|
||||
}
|
||||
|
||||
fun noAnswerCallFromIncomingCall() {
|
||||
onView(withId(R.id.incoming_call_layout)).checkWithTimeout(doesNotExist(), 30.0)
|
||||
}
|
||||
|
||||
fun noAnswerCallFromOutgoingCall() {
|
||||
onView(withId(R.id.outgoing_call_layout)).checkWithTimeout(doesNotExist(), 30.0)
|
||||
/*
|
||||
val snackbar = onView(withId(com.google.android.material.R.id.snackbar_text))
|
||||
snackbar.checkWithTimeout(
|
||||
matches(
|
||||
allOf(
|
||||
withText(containsString("Error")),
|
||||
withText(containsString("Request Timeout"))
|
||||
)
|
||||
),
|
||||
5.0
|
||||
)
|
||||
snackbar.checkWithTimeout(doesNotExist(), 5.0)
|
||||
*/
|
||||
}
|
||||
|
||||
private fun waitForCallNotification(exist: Boolean, timeout: Double) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch(Dispatchers.Default) {
|
||||
lateinit var activity: Activity
|
||||
activityScenario!!.onActivity { act -> activity = act }
|
||||
val manager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
repeat(timeout.toInt() * 10) {
|
||||
for (notif in manager.activeNotifications) {
|
||||
if (notif.notification.channelId == getString(R.string.notification_channel_incoming_call_id)) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
result = false
|
||||
}
|
||||
if (result == exist) { cancel() }
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
wait.join()
|
||||
delay(500)
|
||||
assert(result == exist) { "[UITests] Incoming call Notification still ${if (exist) "not " else ""}displayed after $timeout seconds" }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
package org.linphone.methods
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class UITestsCoreManager {
|
||||
|
||||
var core: Core
|
||||
var accountCreator: AccountCreator
|
||||
private val factory = Factory.instance()
|
||||
|
||||
var appAccountAuthInfo: AuthInfo
|
||||
var ghostAccounts: UITestsGhostAccounts
|
||||
val dnsServer = arrayOf("51.255.123.121")
|
||||
|
||||
companion object {
|
||||
private var mInstance: UITestsCoreManager? = null
|
||||
|
||||
val instance: UITestsCoreManager
|
||||
get() {
|
||||
if (mInstance == null) mInstance = UITestsCoreManager()
|
||||
return mInstance!!
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
factory.loggingService.setLogLevel(LogLevel.Debug)
|
||||
|
||||
// Config account creator for flexiapi
|
||||
val config = factory.createConfig(LinphoneApplication.corePreferences.uiTestsConfigPath)
|
||||
config.setInt("account_creator", "backend", AccountCreatorBackend.FlexiAPI.ordinal)
|
||||
config.setString("account_creator", "url", "http://subscribe.example.org/flexiapi/api/")
|
||||
core = factory.createCoreWithConfig(config, getApplicationContext())
|
||||
core.setDnsServersApp(dnsServer)
|
||||
accountCreator = core.createAccountCreator(null)
|
||||
core.start()
|
||||
ghostAccounts = UITestsGhostAccounts(::newAccountAuthInfo)
|
||||
appAccountAuthInfo = newAccountAuthInfo()
|
||||
ghostAccounts.indexOffset++
|
||||
}
|
||||
|
||||
private fun newAccountAuthInfo(): AuthInfo {
|
||||
val authInfo: AuthInfo
|
||||
if (core.authInfoList.count() > ghostAccounts.count) {
|
||||
authInfo = core.authInfoList[ghostAccounts.count]
|
||||
Log.i("[UITests] Account retrieved (n°${ghostAccounts.count}) {usr: ${authInfo.username}, pwd: ${authInfo.password}, dmn: ${authInfo.domain}}")
|
||||
} else {
|
||||
authInfo = createAccount()
|
||||
}
|
||||
return authInfo
|
||||
}
|
||||
|
||||
fun createAccount(): AuthInfo {
|
||||
accountCreator.username = "uitester_" + (Date().time * 1000).toUInt().toString().subSequence(0, 5)
|
||||
accountCreator.password = (1..15).map { accountCreator.username!!.random() }.joinToString("")
|
||||
accountCreator.domain = "sip.example.org"
|
||||
accountCreator.email = accountCreator.username + "@" + accountCreator.domain
|
||||
accountCreator.transport = TransportType.Tcp
|
||||
accountCreator.createAccount()
|
||||
waitForAccountCreationStatus(AccountCreator.Status.AccountCreated, 5.0)
|
||||
|
||||
val authInfo = factory.createAuthInfo(accountCreator.username!!, "", accountCreator.password, "", "", accountCreator.domain)
|
||||
core.addAuthInfo(authInfo)
|
||||
Log.i("[UITests] New Account created (n°${core.authInfoList.count()}) {usr: ${authInfo.username}, pwd: ${authInfo.password}, dmn: ${authInfo.domain}}")
|
||||
|
||||
return authInfo
|
||||
}
|
||||
|
||||
fun accountsReset() {
|
||||
core.clearAllAuthInfo()
|
||||
ghostAccounts.reset()
|
||||
appAccountAuthInfo = createAccount()
|
||||
}
|
||||
|
||||
fun createAddress(authInfo: AuthInfo): Address {
|
||||
return factory.createAddress("sip:" + authInfo.username + "@" + authInfo.domain)!!
|
||||
}
|
||||
|
||||
fun waitForAccountCreationStatus(wStatus: AccountCreator.Status, timeout: Double) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch { delay(timeout.toLong() * 1000) }
|
||||
val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status?,
|
||||
response: String?
|
||||
) {
|
||||
super.onCreateAccount(creator, status, response)
|
||||
if (wStatus == status) {
|
||||
Log.d("dsqdfs", status.ordinal)
|
||||
result = true
|
||||
wait.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
accountCreator.addListener(listener)
|
||||
wait.join()
|
||||
accountCreator.removeListener(listener)
|
||||
assert(result) { "[UITests] $wStatus account status still not verified after $timeout seconds" }
|
||||
}
|
||||
}
|
||||
|
||||
class UITestsGhostAccounts(authInfoCreationFunction: () -> AuthInfo) {
|
||||
private var mCores = mutableListOf<UITestsRegisteredLinphoneCore>()
|
||||
var indexOffset = 0
|
||||
val count: Int
|
||||
get() = mCores.count() + indexOffset
|
||||
|
||||
private var newCore: (() -> AuthInfo)
|
||||
|
||||
init {
|
||||
newCore = authInfoCreationFunction
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
mCores.clear()
|
||||
}
|
||||
|
||||
operator fun get(index: Int): UITestsRegisteredLinphoneCore {
|
||||
while (index >= mCores.count()) {
|
||||
mCores.add(UITestsRegisteredLinphoneCore(newCore()))
|
||||
}
|
||||
return mCores[index]
|
||||
}
|
||||
}
|
||||
|
||||
class UITestsRegisteredLinphoneCore(authInfo: AuthInfo) {
|
||||
var mCore: Core
|
||||
private val factory = Factory.instance()
|
||||
var description: String
|
||||
|
||||
private val manager = UITestsCoreManager.instance
|
||||
|
||||
var mCoreListener: CoreListener private set
|
||||
lateinit var mAccount: Account private set
|
||||
var mAuthInfo: AuthInfo private set
|
||||
|
||||
var callState = Call.State.Released
|
||||
private set
|
||||
var registrationState = RegistrationState.Cleared
|
||||
private set
|
||||
|
||||
init {
|
||||
description = "Ghost Account (" + authInfo.username + ")"
|
||||
factory.loggingService.setLogLevel(LogLevel.Debug)
|
||||
|
||||
mCore = factory.createCore("", "", getApplicationContext())
|
||||
mCore.setDnsServersApp(manager.dnsServer)
|
||||
|
||||
mCore.isVideoCaptureEnabled = true
|
||||
mCore.isVideoDisplayEnabled = true
|
||||
mCore.isRecordAwareEnabled = true
|
||||
mCore.videoActivationPolicy.automaticallyAccept = true
|
||||
|
||||
mCoreListener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State?,
|
||||
message: String
|
||||
) {
|
||||
callState = state ?: Call.State.Released
|
||||
Log.i("[UITests] ${authInfo.username} current call state is $callState")
|
||||
}
|
||||
|
||||
override fun onAccountRegistrationStateChanged(
|
||||
core: Core,
|
||||
account: Account,
|
||||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
registrationState = state ?: RegistrationState.Cleared
|
||||
Log.i("[UITests] New registration state \"$state\" for user ${account.params.identityAddress?.username}")
|
||||
}
|
||||
}
|
||||
mCore.addListener(mCoreListener)
|
||||
|
||||
mCore.playFile = "sounds/hello8000.wav"
|
||||
mCore.useFiles = true
|
||||
|
||||
mCore.start()
|
||||
|
||||
mAuthInfo = authInfo
|
||||
login(TransportType.Tcp)
|
||||
}
|
||||
|
||||
fun login(transport: TransportType) {
|
||||
val accountParams = mCore.createAccountParams()
|
||||
val identity = manager.createAddress(mAuthInfo)
|
||||
accountParams.setIdentityAddress(identity)
|
||||
val address = factory.createAddress("sip:" + mAuthInfo.domain)!!
|
||||
address.transport = transport
|
||||
accountParams.serverAddress = address
|
||||
accountParams.isRegisterEnabled = true
|
||||
val account = mCore.createAccount(accountParams)
|
||||
mCore.addAuthInfo(mAuthInfo)
|
||||
mCore.addAccount(account)
|
||||
mAccount = account
|
||||
mCore.defaultAccount = mAccount
|
||||
waitForRegistrationState(RegistrationState.Ok, 5.0)
|
||||
}
|
||||
|
||||
fun startCall(address: Address) {
|
||||
val params = mCore.createCallParams(null)!!
|
||||
params.mediaEncryption = MediaEncryption.None
|
||||
params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(address)
|
||||
mCore.inviteAddressWithParams(address, params)
|
||||
}
|
||||
|
||||
fun terminateCall() {
|
||||
if (mCore.callsNb == 0) { return }
|
||||
val call = if (mCore.currentCall != null) mCore.currentCall else mCore.calls[0]
|
||||
call ?: return
|
||||
call.terminate()
|
||||
}
|
||||
|
||||
fun acceptCall() {
|
||||
mCore.currentCall?.accept()
|
||||
}
|
||||
|
||||
fun toggleMicrophone() {
|
||||
mCore.isMicEnabled = !mCore.isMicEnabled
|
||||
}
|
||||
|
||||
fun toggleSpeaker() {
|
||||
val currentAudioDevice = mCore.currentCall?.outputAudioDevice
|
||||
val speakerEnabled = currentAudioDevice?.type == AudioDevice.Type.Speaker
|
||||
|
||||
for (audioDevice in mCore.audioDevices) {
|
||||
if (speakerEnabled && audioDevice.type == AudioDevice.Type.Earpiece) {
|
||||
mCore.currentCall?.outputAudioDevice = audioDevice
|
||||
return
|
||||
} else if (!speakerEnabled && audioDevice.type == AudioDevice.Type.Speaker) {
|
||||
mCore.currentCall?.outputAudioDevice = audioDevice
|
||||
return
|
||||
} /* If we wanted to route the audio to a bluetooth headset
|
||||
else if (audioDevice.type == AudioDevice.Type.Bluetooth) {
|
||||
core.currentCall?.outputAudioDevice = audioDevice
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVideo() {
|
||||
if (mCore.callsNb == 0) return
|
||||
val call = if (mCore.currentCall != null) mCore.currentCall else mCore.calls[0]
|
||||
call ?: return
|
||||
|
||||
val params = mCore.createCallParams(call)
|
||||
params?.isVideoEnabled = !call.currentParams.isVideoEnabled
|
||||
call.update(params)
|
||||
}
|
||||
|
||||
fun toggleCamera() {
|
||||
val currentDevice = mCore.videoDevice
|
||||
for (camera in mCore.videoDevicesList) {
|
||||
if (camera != currentDevice && camera != "StaticImage: Static picture") {
|
||||
mCore.videoDevice = camera
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseCall() {
|
||||
if (mCore.callsNb == 0) return
|
||||
val call = if (mCore.currentCall != null) mCore.currentCall else mCore.calls[0]
|
||||
call ?: return
|
||||
call.pause()
|
||||
}
|
||||
|
||||
fun resumeCall() {
|
||||
if (mCore.callsNb == 0) return
|
||||
val call = if (mCore.currentCall != null) mCore.currentCall else mCore.calls[0]
|
||||
call ?: return
|
||||
call.resume()
|
||||
}
|
||||
|
||||
fun startRecording() {
|
||||
mCore.currentCall?.startRecording()
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
mCore.currentCall?.stopRecording()
|
||||
}
|
||||
|
||||
fun waitForRegistrationState(registrationState: RegistrationState, timeout: Double) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch { delay(timeout.toLong() * 1000) }
|
||||
val listener = object : AccountListenerStub() {
|
||||
override fun onRegistrationStateChanged(
|
||||
account: Account,
|
||||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
super.onRegistrationStateChanged(account, state, message)
|
||||
if (registrationState == state) {
|
||||
result = true
|
||||
wait.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
mCore.defaultAccount!!.addListener(listener)
|
||||
wait.join()
|
||||
mCore.defaultAccount!!.removeListener(listener)
|
||||
assert(result) { "[UITests] $registrationState registration state still not verified after $timeout seconds" }
|
||||
}
|
||||
|
||||
fun waitForCallState(callState: Call.State, timeout: Double) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch { delay(timeout.toLong() * 1000) }
|
||||
val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State?,
|
||||
message: String
|
||||
) {
|
||||
super.onCallStateChanged(core, call, state, message)
|
||||
if (callState == state) {
|
||||
result = true
|
||||
wait.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
mCore.addListener(listener)
|
||||
wait.join()
|
||||
mCore.removeListener(listener)
|
||||
assert(result) { "[UITests] $callState call state still not verified after $timeout seconds" }
|
||||
}
|
||||
|
||||
fun waitForRecordingState(recording: Boolean, onRemote: Boolean = false, timeout: Double) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch(Dispatchers.Default) {
|
||||
repeat(timeout.toInt() * 10) { i ->
|
||||
if (!onRemote && recording == mCore.currentCall?.params?.isRecording) {
|
||||
result = true
|
||||
cancel()
|
||||
}
|
||||
if (onRemote && recording == mCore.currentCall?.remoteParams?.isRecording) {
|
||||
result = true
|
||||
cancel()
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
val remoteText = if (onRemote) "remote" else ""
|
||||
wait.join()
|
||||
assert(result) { "[UITests] $remoteText call state still not $recording after ${timeout.toInt()} seconds" }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package org.linphone.methods
|
||||
|
||||
import android.os.Environment.DIRECTORY_PICTURES
|
||||
import android.os.Environment.getExternalStoragePublicDirectory
|
||||
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor
|
||||
import androidx.test.runner.screenshot.Screenshot
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
object UITestsScreenshots {
|
||||
|
||||
var defaultPath = File(getExternalStoragePublicDirectory(DIRECTORY_PICTURES), "")
|
||||
var screenshotComparison = true
|
||||
|
||||
fun definePath(testClass: String, testFunction: String, startTime: String) {
|
||||
defaultPath = File(
|
||||
File(
|
||||
getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
|
||||
"linphone_uitests"
|
||||
).absolutePath,
|
||||
"$testClass/$testFunction/$startTime"
|
||||
)
|
||||
}
|
||||
|
||||
private fun screenshot(screenShotName: String) {
|
||||
Log.i("[UITests] Taking screenshot of '$screenShotName'")
|
||||
val screenCapture = Screenshot.capture()
|
||||
val processors = setOf(MyScreenCaptureProcessor())
|
||||
try {
|
||||
screenCapture.apply {
|
||||
name = screenShotName
|
||||
process(processors)
|
||||
}
|
||||
Log.i("[UITests] Screenshot taken")
|
||||
} catch (ex: IOException) {
|
||||
Log.e("[UITests] Could not take a screenshot", ex)
|
||||
}
|
||||
}
|
||||
|
||||
fun takeScreenshot(
|
||||
name: String,
|
||||
variant: String? = null,
|
||||
line: Int = Throwable().stackTrace[1].lineNumber
|
||||
) {
|
||||
if (!screenshotComparison) return
|
||||
if (name.contains(".") || variant?.contains(".") == true) {
|
||||
throw Exception("[UITests] \".\" character is forbidden for screencheck methods arguments name and variant")
|
||||
}
|
||||
screenshot(line.toString() + ".$name" + if (variant != null) ".$variant" else "")
|
||||
}
|
||||
}
|
||||
|
||||
class MyScreenCaptureProcessor : BasicScreenCaptureProcessor() {
|
||||
|
||||
init {
|
||||
this.mDefaultScreenshotPath = UITestsScreenshots.defaultPath
|
||||
}
|
||||
|
||||
override fun getFilename(prefix: String): String = prefix
|
||||
}
|
||||
167
app/src/androidTest/java/org/linphone/methods/UITestsUtils.kt
Normal file
167
app/src/androidTest/java/org/linphone/methods/UITestsUtils.kt
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package org.linphone.methods
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import androidx.test.espresso.*
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.hamcrest.Matcher
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel
|
||||
import org.linphone.activities.main.viewmodels.StatusViewModel
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.TransportType
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class ScreenshotsRule(active: Boolean) : TestWatcher() {
|
||||
|
||||
val screenshotComparison = active
|
||||
|
||||
override fun starting(description: Description) {
|
||||
super.starting(description)
|
||||
UITestsScreenshots.screenshotComparison = screenshotComparison
|
||||
UITestsScreenshots.definePath(description.className, description.methodName, Date().time.toString())
|
||||
if (screenshotComparison && !UITestsScreenshots.defaultPath.isDirectory) {
|
||||
UITestsScreenshots.defaultPath.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UITestsUtils {
|
||||
|
||||
private var mainActivityIntent = Intent(getApplicationContext(), MainActivity::class.java)
|
||||
var activityScenario: ActivityScenario<MainActivity>? = null
|
||||
|
||||
fun testAppSetup() {
|
||||
// launch app
|
||||
Log.i("[UITests] Launch Linphone app")
|
||||
if (!isAppLaunch()) { launchApp() }
|
||||
if (!rightAccountConnected() || !accountIsConnected()) {
|
||||
removeAllAccounts()
|
||||
connectAccount()
|
||||
assert(accountIsConnected()) { "registration state on the Status Bar is still not : Connected after 10 seconds" }
|
||||
}
|
||||
onView(withId(R.id.dialer_layout)).checkWithTimeout(matches(isDisplayed()), 5.0)
|
||||
}
|
||||
|
||||
fun launchApp() {
|
||||
if (isAppLaunch()) activityScenario?.close()
|
||||
activityScenario = ActivityScenario.launch(mainActivityIntent)
|
||||
}
|
||||
|
||||
fun isAppLaunch(): Boolean {
|
||||
if (activityScenario != null) return activityScenario!!.state != Lifecycle.State.DESTROYED
|
||||
return false
|
||||
}
|
||||
|
||||
fun accountIsConnected(): Boolean {
|
||||
var result = false
|
||||
runBlocking {
|
||||
val wait = launch { delay(5000) }
|
||||
val observer = Observer<Int> {
|
||||
if (it == R.string.status_connected) {
|
||||
result = true
|
||||
wait.cancel()
|
||||
}
|
||||
}
|
||||
lateinit var viewModel: StatusViewModel
|
||||
getInstrumentation().runOnMainSync {
|
||||
viewModel = StatusViewModel()
|
||||
viewModel.registrationStatusText.observeForever(observer)
|
||||
}
|
||||
wait.join()
|
||||
getInstrumentation().runOnMainSync { viewModel.registrationStatusText.removeObserver(observer) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun rightAccountConnected(): Boolean {
|
||||
val realAccount = LinphoneApplication.coreContext.core.defaultAccount?.findAuthInfo()?.username
|
||||
val expectedAccount = UITestsCoreManager.instance.appAccountAuthInfo.username
|
||||
return realAccount == expectedAccount
|
||||
}
|
||||
|
||||
fun connectAccount() {
|
||||
val manager = UITestsCoreManager.instance
|
||||
manager.accountsReset()
|
||||
Log.i("[UITests] Connect ${manager.appAccountAuthInfo.username} user to Linphone app")
|
||||
val core = LinphoneApplication.coreContext.core
|
||||
LinphoneApplication.corePreferences.useDnsServer = true
|
||||
LinphoneApplication.corePreferences.dnsServerAddress = manager.dnsServer.first()
|
||||
val accountParams = core.createAccountParams()
|
||||
val identity = manager.createAddress(manager.appAccountAuthInfo)
|
||||
accountParams.identityAddress = identity
|
||||
val address = Factory.instance().createAddress("sip:" + manager.appAccountAuthInfo.domain)!!
|
||||
address.transport = TransportType.Tcp
|
||||
accountParams.serverAddress = address
|
||||
accountParams.isRegisterEnabled = true
|
||||
val account = core.createAccount(accountParams)
|
||||
core.addAuthInfo(manager.appAccountAuthInfo)
|
||||
core.addAccount(account)
|
||||
core.defaultAccount = account
|
||||
}
|
||||
|
||||
fun removeAllAccounts() {
|
||||
Log.i("[UITests] Remove all accounts from the Linphone app core")
|
||||
for (account in LinphoneApplication.coreContext.core.accountList) {
|
||||
getInstrumentation().runOnMainSync {
|
||||
val viewModel = AccountSettingsViewModel(account)
|
||||
viewModel.deleteListener.onClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForExistence(matcher: Matcher<View>, timeout: Double) {
|
||||
return waitForView(matcher, timeout, true)
|
||||
}
|
||||
|
||||
fun waitForNonExistence(matcher: Matcher<View>, timeout: Double) {
|
||||
return waitForView(matcher, timeout, false)
|
||||
}
|
||||
|
||||
private fun waitForView(matcher: Matcher<View>, timeout: Double, exist: Boolean) = runBlocking {
|
||||
var result = false
|
||||
val wait = launch(Dispatchers.Default) {
|
||||
repeat(timeout.toInt() * 10) {
|
||||
try {
|
||||
onView(matcher).check(matches(isDisplayed()))
|
||||
result = true
|
||||
cancel()
|
||||
} catch (_: Exception) {
|
||||
// do nothing to retry until timeout
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
wait.join()
|
||||
assert(result) { "[UITests] $matcher still ${if (exist) "not " else ""}displayed after $timeout seconds" }
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkWithTimeout(viewAssert: ViewAssertion, timeout: Double): ViewInteraction = runBlocking {
|
||||
val wait = launch(Dispatchers.Default) {
|
||||
repeat(timeout.toInt() * 10) {
|
||||
try {
|
||||
check(viewAssert)
|
||||
cancel()
|
||||
} catch (e: Throwable) {
|
||||
// do nothing to retry until timeout
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
wait.join()
|
||||
check(viewAssert)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
package org.linphone.testsuites
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Suite
|
||||
import org.linphone.call.OutgoingCallUITests
|
||||
|
||||
@RunWith(Suite::class)
|
||||
@Suite.SuiteClasses(
|
||||
OutgoingCallUITests::class
|
||||
)
|
||||
class CallTestSuite
|
||||
|
|
@ -21,6 +21,7 @@ package org.linphone.activities.main.settings.viewmodels
|
|||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.lang.NumberFormatException
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.activities.main.settings.SettingListenerStub
|
||||
|
||||
class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
||||
|
|
@ -58,11 +59,29 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
|||
}
|
||||
val sipPort = MutableLiveData<Int>()
|
||||
|
||||
val useDnsServerListener = object : SettingListenerStub() {
|
||||
override fun onBoolValueChanged(newValue: Boolean) {
|
||||
if (newValue) core.setDnsServersApp(arrayOf(dnsServerAddress.value))
|
||||
else core.setDnsServersApp(arrayOf())
|
||||
LinphoneApplication.corePreferences.useDnsServer = newValue
|
||||
}
|
||||
}
|
||||
val useDnsServer = MutableLiveData<Boolean>()
|
||||
|
||||
val dnsServerAddressListener = object : SettingListenerStub() {
|
||||
override fun onTextValueChanged(newValue: String) {
|
||||
LinphoneApplication.corePreferences.dnsServerAddress = newValue
|
||||
}
|
||||
}
|
||||
val dnsServerAddress = MutableLiveData<String>()
|
||||
|
||||
init {
|
||||
wifiOnly.value = core.isWifiOnlyEnabled
|
||||
allowIpv6.value = core.isIpv6Enabled
|
||||
randomPorts.value = getTransportPort() == -1
|
||||
sipPort.value = getTransportPort()
|
||||
useDnsServer.value = LinphoneApplication.corePreferences.useDnsServer
|
||||
dnsServerAddress.value = LinphoneApplication.corePreferences.dnsServerAddress
|
||||
}
|
||||
|
||||
private fun setTransportPort(port: Int) {
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
|
|||
private fun updateHingeRelatedConstraints(feature: FoldingFeature) {
|
||||
Log.i("[Single Call] Updating constraint layout hinges: $feature")
|
||||
|
||||
val constraintLayout = binding.constraintLayout
|
||||
val constraintLayout = binding.singleCallLayout
|
||||
val set = ConstraintSet()
|
||||
set.clone(constraintLayout)
|
||||
|
||||
|
|
|
|||
|
|
@ -429,6 +429,11 @@ class CoreContext(
|
|||
core.config.setInt("misc", "conference_layout", 1)
|
||||
}
|
||||
|
||||
// Dns Server init
|
||||
if (core.config.getBool("app", "use_custom_dns_server", false)) {
|
||||
core.setDnsServersApp(arrayOf(core.config.getString("app", "custom_dns_address", "") ?: ""))
|
||||
}
|
||||
|
||||
// Now LIME server URL is set on accounts
|
||||
val limeServerUrl = core.limeX3DhServerUrl.orEmpty()
|
||||
if (limeServerUrl.isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,18 @@ class CorePreferences constructor(private val context: Context) {
|
|||
config.setBool("app", "read_and_agree_terms_and_privacy", value)
|
||||
}
|
||||
|
||||
var useDnsServer: Boolean
|
||||
get() = config.getBool("app", "use_custom_dns_server", false)
|
||||
set(value) {
|
||||
config.setBool("app", "use_custom_dns_server", value)
|
||||
}
|
||||
|
||||
var dnsServerAddress: String
|
||||
get() = config.getString("app", "custom_dns_address", "") ?: ""
|
||||
set(value) {
|
||||
config.setString("app", "custom_dns_address", value)
|
||||
}
|
||||
|
||||
/* UI */
|
||||
|
||||
var forcePortrait: Boolean
|
||||
|
|
@ -643,6 +655,9 @@ class CorePreferences constructor(private val context: Context) {
|
|||
val configPath: String
|
||||
get() = context.filesDir.absolutePath + "/.linphonerc"
|
||||
|
||||
val uiTestsConfigPath: String
|
||||
get() = context.filesDir.absolutePath + "/.linphonerc_uitests"
|
||||
|
||||
val factoryConfigPath: String
|
||||
get() = context.filesDir.absolutePath + "/linphonerc"
|
||||
|
||||
|
|
|
|||
1
screport
Submodule
1
screport
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 58525a25b6a9bcbe79e451dced5e9424b21424cc
|
||||
Loading…
Add table
Reference in a new issue