Skip to content

Commit

Permalink
Android UI instrumented tests (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdsantos authored Oct 9, 2024
1 parent b9045fa commit 5da6e1c
Show file tree
Hide file tree
Showing 23 changed files with 781 additions and 71 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_import-ordering = disabled

[*.yml]
indent_size = 2
27 changes: 27 additions & 0 deletions .github/workflows/android_instrumented_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Android Instrumented Tests
on: workflow_dispatch

jobs:
android-instrumented-tests:
name: Android Instrumented Tests
runs-on: macos-13

steps:
- uses: actions/checkout@v4

- name: Setup
uses: ./.github/actions/setup

- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
arch: x86_64
script: ./gradlew copyBrandingToCommonResources connectedDebugAndroidTest

- name: Uploads test reports
uses: actions/upload-artifact@v4
if: failure()
with:
name: android-instrumented-tests-report
path: composeApp/build/reports/androidTests/connected/debug/
64 changes: 32 additions & 32 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:

strategy:
matrix:
type: [Debug, Release]
organization: [ooni, dw]
type: [ Debug ]
organization: [ ooni, dw ]

steps:
- uses: actions/checkout@v4
Expand All @@ -29,7 +29,7 @@ jobs:

- name: Uploads artifacts
uses: actions/upload-artifact@v4
if: matrix.type == 'Debug' # we want just the debug apk. Releases have to be signed and uploaded to the stores
if: matrix.type == 'Debug' # we want just the debug apk. Releases have to be signed and uploaded to the stores
with:
name: ${{ matrix.organization }}-${{ matrix.type }}-APK
path: composeApp/build/outputs/apk/debug/composeApp-debug.apk
Expand Down Expand Up @@ -98,32 +98,32 @@ jobs:
path: composeApp/build/reports/tests/iosSimulatorArm64Test/

distribute:
name: Firebase App Distribution
runs-on: ubuntu-latest
needs: [ common-tests ]
strategy:
matrix:
organization: [ooni, dw]
steps:
- uses: actions/checkout@v4

- name: Download app APK
uses: actions/download-artifact@v4
with:
name: ${{ matrix.organization }}-Debug-APK

- name: Firebase App Distribution
id: uploadArtifact
env:
INPUT_APP_ID: ${{ fromJSON(secrets.FIREBASE_APP_ID)[matrix.organization] }}
INPUT_SERVICE_CREDENTIALS_FILE_CONTENT: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
GOOGLE_APPLICATION_CREDENTIALS: service_credentials_content.json
INPUT_GROUPS: testers
INPUT_FILE: composeApp-debug.apk
run: |
cat <<< "${INPUT_SERVICE_CREDENTIALS_FILE_CONTENT}" > service_credentials_content.json
sudo npm install -g firebase-tools
OUTPUT=$(firebase appdistribution:distribute "$INPUT_FILE" --app "$INPUT_APP_ID" --groups "$INPUT_GROUPS" --testers "$INPUT_TESTERS" --release-notes "$(git show -s --format='%an <%ae> , Hash : %H, Message : %s')")
echo "$OUTPUT"
DASHBOARD_URL=$(echo "$OUTPUT" | grep -o 'https://console.firebase.google.com/.*')
echo "Dashboard URL( ${{ matrix.organization }} ): $DASHBOARD_URL" >> $GITHUB_STEP_SUMMARY
name: Firebase App Distribution
runs-on: ubuntu-latest
needs: [ common-tests ]
strategy:
matrix:
organization: [ ooni, dw ]
steps:
- uses: actions/checkout@v4

- name: Download app APK
uses: actions/download-artifact@v4
with:
name: ${{ matrix.organization }}-Debug-APK

- name: Firebase App Distribution
id: uploadArtifact
env:
INPUT_APP_ID: ${{ fromJSON(secrets.FIREBASE_APP_ID)[matrix.organization] }}
INPUT_SERVICE_CREDENTIALS_FILE_CONTENT: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
GOOGLE_APPLICATION_CREDENTIALS: service_credentials_content.json
INPUT_GROUPS: testers
INPUT_FILE: composeApp-debug.apk
run: |
cat <<< "${INPUT_SERVICE_CREDENTIALS_FILE_CONTENT}" > service_credentials_content.json
sudo npm install -g firebase-tools
OUTPUT=$(firebase appdistribution:distribute "$INPUT_FILE" --app "$INPUT_APP_ID" --groups "$INPUT_GROUPS" --testers "$INPUT_TESTERS" --release-notes "$(git show -s --format='%an <%ae> , Hash : %H, Message : %s')")
echo "$OUTPUT"
DASHBOARD_URL=$(echo "$OUTPUT" | grep -o 'https://console.firebase.google.com/.*')
echo "Dashboard URL( ${{ matrix.organization }} ): $DASHBOARD_URL" >> $GITHUB_STEP_SUMMARY
4 changes: 4 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ kotlin {
implementation(kotlin("test-junit"))
implementation(libs.bundles.android.test)
}
getByName("androidInstrumentedTest").dependencies {
implementation(libs.bundles.android.instrumented.test)
}
all {
languageSettings {
optIn("kotlin.ExperimentalStdlibApi")
Expand Down Expand Up @@ -173,6 +176,7 @@ android {
config.supportedLanguages.joinToString(separator = ","),
)
resourceConfigurations += config.supportedLanguages
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
packaging {
resources {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.ooni.probe.test

import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.ooni.probe.data.models.SettingsKey
import org.ooni.probe.test.helpers.CleanTestRule
import org.ooni.probe.test.helpers.FlakyTestRule
import org.ooni.probe.test.helpers.clickOnTag
import org.ooni.probe.test.helpers.clickOnText
import org.ooni.probe.test.helpers.dependencies
import org.ooni.probe.test.helpers.preferences
import org.ooni.probe.test.helpers.start
import org.ooni.probe.test.helpers.wait

@RunWith(AndroidJUnit4::class)
class OnboardingTest {
@get:Rule
val clean = CleanTestRule()

@get:Rule
val flakyTestRule = FlakyTestRule()

@get:Rule
val compose = createEmptyComposeRule()

@Before
fun setUp() {
start()
}

@Test
fun onboarding() =
runTest {
with(compose) {
wait { onNodeWithText("What is OONI Probe?").isDisplayed() }
clickOnText("Got It")

wait { onNodeWithText("Heads-up!").isDisplayed() }
clickOnText("I understand")

// Quiz
clickOnText("True")
clickOnText("True")

wait { onNodeWithText("Automated testing").isDisplayed() }
clickOnTag("Yes-AutoTest")

wait { onNodeWithText("Crash Reporting").isDisplayed() }
clickOnTag("Yes-CrashReporting")

if (dependencies.platformInfo.needsToRequestNotificationsPermission) {
wait { onNodeWithText("Get updates on internet censorship").isDisplayed() }
clickOnTag("No-Notifications")
}

wait { onNodeWithText("Default Settings").isDisplayed() }
clickOnText("Let’s go")

wait { onNodeWithContentDescription("OONI Probe").isDisplayed() }
}

assertEquals(
true,
preferences.getValueByKey(SettingsKey.AUTOMATED_TESTING_ENABLED).first(),
)
assertEquals(true, preferences.getValueByKey(SettingsKey.SEND_CRASH).first())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package org.ooni.probe.test

import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.ooni.probe.data.models.SettingsKey
import org.ooni.probe.test.helpers.CleanTestRule
import org.ooni.probe.test.helpers.FlakyTestRule
import org.ooni.probe.test.helpers.checkLinkInsideWebView
import org.ooni.probe.test.helpers.checkSummaryInsideWebView
import org.ooni.probe.test.helpers.clickOnText
import org.ooni.probe.test.helpers.preferences
import org.ooni.probe.test.helpers.skipOnboarding
import org.ooni.probe.test.helpers.start
import org.ooni.probe.test.helpers.wait
import kotlin.time.Duration.Companion.minutes

@RunWith(AndroidJUnit4::class)
class RunningTest {
@get:Rule
val clean = CleanTestRule()

@get:Rule
val flakyTestRule = FlakyTestRule()

@get:Rule
val compose = createEmptyComposeRule()

@Before
fun setUp() =
runTest {
skipOnboarding()
preferences.setValueByKey(SettingsKey.UPLOAD_RESULTS, true)
start()
}

@Test
fun signal() =
runTest {
with(compose) {
clickOnText("Run")

clickOnText("Deselect all tests")
clickOnText("Signal Test")
clickOnText("Run 1 test")

wait(TEST_WAIT_TIMEOUT) {
onNodeWithText("Run finished. Tap to view results.").isDisplayed()
}
clickOnText("Run finished. Tap to view results.")

clickOnText("Instant Messaging")
clickOnText("Signal Test")
wait { onNodeWithText("Measurement").isDisplayed() }
checkSummaryInsideWebView("Signal")
}
}

@Test
fun psiphon() =
runTest {
with(compose) {
clickOnText("Run")

clickOnText("Deselect all tests")
clickOnText("Psiphon Test")
clickOnText("Run 1 test")

wait(TEST_WAIT_TIMEOUT) {
onNodeWithText("Run finished. Tap to view results.").isDisplayed()
}
clickOnText("Run finished. Tap to view results.")

clickOnText("Circumvention")
clickOnText("Psiphon Test")
wait { onNodeWithText("Measurement").isDisplayed() }
checkSummaryInsideWebView("Psiphon")
}
}

@Test
fun httpHeader() =
runTest {
with(compose) {
clickOnText("Run")

clickOnText("Deselect all tests")
onNodeWithTag("Run-DescriptorsList")
.performScrollToNode(hasText("HTTP Header", substring = true))
clickOnText("HTTP Header", substring = true)
clickOnText("Run 1 test")

wait(TEST_WAIT_TIMEOUT) {
onNodeWithText("Run finished. Tap to view results.").isDisplayed()
}
clickOnText("Run finished. Tap to view results.")

clickOnText("Performance")
clickOnText("HTTP Header", substring = true)
wait { onNodeWithText("Measurement").isDisplayed() }
checkSummaryInsideWebView("middleboxes")
}
}

@Test
fun stunReachability() =
runTest {
with(compose) {
clickOnText("Run")

clickOnText("Deselect all tests")
onNodeWithTag("Run-DescriptorsList")
.performScrollToNode(hasText("stunreachability"))
clickOnText("stunreachability", substring = true)
clickOnText("Run 1 test")

wait(TEST_WAIT_TIMEOUT) {
onNodeWithText("Run finished. Tap to view results.").isDisplayed()
}
clickOnText("Run finished. Tap to view results.")

clickOnText("Experimental")
compose.onAllNodesWithText("stunreachability")[0].performClick()
wait { onNodeWithText("Measurement").isDisplayed() }
checkLinkInsideWebView("https://ooni.org/nettest/http-requests/", "STUN Reachability")
}
}

companion object {
private val TEST_WAIT_TIMEOUT = 1.minutes
}
}
Loading

0 comments on commit 5da6e1c

Please sign in to comment.