diff --git a/.github/workflows/instrumented-tests.yml b/.github/workflows/instrumented-tests.yml index 275acc1c..627303c8 100644 --- a/.github/workflows/instrumented-tests.yml +++ b/.github/workflows/instrumented-tests.yml @@ -1,14 +1,17 @@ name: Android Instrumented Tests on: push: - branches: - - main workflow_dispatch: jobs: instrumentation-tests: name: Run Android Instrumented Tests runs-on: macos-latest + + strategy: + matrix: + organization: [ ooni, dw ] + steps: - uses: actions/checkout@v4 @@ -16,7 +19,7 @@ jobs: uses: ./.github/actions/setup - name: Build APKs - run: ./gradlew copyBrandingToCommonResources assembleFullDebug assembleFullDebugAndroidTest + run: ./gradlew copyBrandingToCommonResources assembleFullDebug assembleFullDebugAndroidTest -Porganization=${{ matrix.organization }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt index 41cf5c1b..6cfd5b3e 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt @@ -18,6 +18,14 @@ import androidx.compose.ui.test.swipeDown import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import ooniprobe.composeapp.generated.resources.AddDescriptor_Action +import ooniprobe.composeapp.generated.resources.AddDescriptor_AutoUpdate +import ooniprobe.composeapp.generated.resources.AddDescriptor_Title +import ooniprobe.composeapp.generated.resources.Dashboard_Progress_ReviewLink_Action +import ooniprobe.composeapp.generated.resources.Dashboard_ReviewDescriptor_Button_Last +import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_UninstallLink +import ooniprobe.composeapp.generated.resources.Res +import org.jetbrains.compose.resources.getString import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -27,9 +35,13 @@ import org.junit.runner.RunWith import org.ooni.engine.OonimkallBridge import org.ooni.engine.TestOonimkallBridge import org.ooni.probe.MainActivity +import org.ooni.probe.config.OrganizationConfig import org.ooni.probe.uitesting.helpers.clickOnText import org.ooni.probe.uitesting.helpers.context import org.ooni.probe.uitesting.helpers.dependencies +import org.ooni.probe.uitesting.helpers.isNewsMediaScan +import org.ooni.probe.uitesting.helpers.onAllNodesWithText +import org.ooni.probe.uitesting.helpers.onNodeWithText import org.ooni.probe.uitesting.helpers.preferences import org.ooni.probe.uitesting.helpers.skipOnboarding import org.ooni.probe.uitesting.helpers.start @@ -50,6 +62,8 @@ class DescriptorsTest { @Test fun installAndUninstall() { runTest { + if (isNewsMediaScan) return@runTest + start( Intent(context, MainActivity::class.java) .setAction(Intent.ACTION_VIEW) @@ -58,7 +72,7 @@ class DescriptorsTest { with(compose) { wait(DESCRIPTOR_DOWNLOAD_WAIT_TIMEOUT) { - onNodeWithText("Install New Link").isDisplayed() + onNodeWithText(Res.string.AddDescriptor_Title).isDisplayed() } onNodeWithText("Testing").assertIsDisplayed() @@ -81,9 +95,10 @@ class DescriptorsTest { assertTrue(preferences.isNetTestEnabled(descriptor, test, isAutoRun = true).first()) clickOnText("Android instrumented tests") - clickOnText("Uninstall Link") + clickOnText(Res.string.Dashboard_Runv2_Overview_UninstallLink) - compose.onAllNodesWithText("Uninstall Link").onLast().performClick() + onAllNodesWithText(Res.string.Dashboard_Runv2_Overview_UninstallLink).onLast() + .performClick() onNodeWithText("Testing").assertIsNotDisplayed() } @@ -93,6 +108,8 @@ class DescriptorsTest { @Test fun installAndUpdate() = runTest { + if (isNewsMediaScan) return@runTest + start( Intent(context, MainActivity::class.java) .setAction(Intent.ACTION_VIEW) @@ -101,10 +118,10 @@ class DescriptorsTest { with(compose) { wait(DESCRIPTOR_DOWNLOAD_WAIT_TIMEOUT) { - onNodeWithText("Install New Link").isDisplayed() + onNodeWithText(Res.string.AddDescriptor_Title).isDisplayed() } - clickOnText("Install updates automatically") - clickOnText("Install Link") + clickOnText(Res.string.AddDescriptor_AutoUpdate) + clickOnText(Res.string.AddDescriptor_Action) Thread.sleep(2000) @@ -114,15 +131,14 @@ class DescriptorsTest { // Pull down to refresh onNodeWithTag("Dashboard-List").performTouchInput { swipeDown() } - wait(DESCRIPTOR_DOWNLOAD_WAIT_TIMEOUT) { - onNodeWithText("Review").isDisplayed() - } - - clickOnText("Review") + clickOnText( + Res.string.Dashboard_Progress_ReviewLink_Action, + timeout = DESCRIPTOR_DOWNLOAD_WAIT_TIMEOUT + ) wait { onNodeWithText("Testing 2").isDisplayed() } - clickOnText("UPDATE AND FINISH (1 of 1)") + clickOnText(getString(Res.string.Dashboard_ReviewDescriptor_Button_Last, 1, 1)) onNodeWithTag("Dashboard-List") .performScrollToNode(hasText("Android instrumented tests")) diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/OnboardingTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/OnboardingTest.kt index 62b319a9..6587988b 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/OnboardingTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/OnboardingTest.kt @@ -2,11 +2,21 @@ package org.ooni.probe.uitesting 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 ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Title +import ooniprobe.composeapp.generated.resources.Onboarding_AutomatedTesting_Title +import ooniprobe.composeapp.generated.resources.Onboarding_Crash_Title +import ooniprobe.composeapp.generated.resources.Onboarding_DefaultSettings_Button_Go +import ooniprobe.composeapp.generated.resources.Onboarding_DefaultSettings_Title +import ooniprobe.composeapp.generated.resources.Onboarding_PopQuiz_True +import ooniprobe.composeapp.generated.resources.Onboarding_ThingsToKnow_Button +import ooniprobe.composeapp.generated.resources.Onboarding_ThingsToKnow_Title +import ooniprobe.composeapp.generated.resources.Onboarding_WhatIsOONIProbe_GotIt +import ooniprobe.composeapp.generated.resources.Onboarding_WhatIsOONIProbe_Title +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.app_name import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -16,6 +26,8 @@ import org.ooni.probe.data.models.SettingsKey import org.ooni.probe.uitesting.helpers.clickOnTag import org.ooni.probe.uitesting.helpers.clickOnText import org.ooni.probe.uitesting.helpers.dependencies +import org.ooni.probe.uitesting.helpers.onNodeWithContentDescription +import org.ooni.probe.uitesting.helpers.onNodeWithText import org.ooni.probe.uitesting.helpers.preferences import org.ooni.probe.uitesting.helpers.start import org.ooni.probe.uitesting.helpers.wait @@ -35,31 +47,39 @@ class OnboardingTest { fun onboarding() = runTest { with(compose) { - wait { onNodeWithText("What is OONI Probe?").isDisplayed() } - clickOnText("Got It") + wait { + onNodeWithText(Res.string.Onboarding_WhatIsOONIProbe_Title) + .isDisplayed() + } + clickOnText(Res.string.Onboarding_WhatIsOONIProbe_GotIt) - wait { onNodeWithText("Heads-up!").isDisplayed() } - clickOnText("I understand") + wait { + onNodeWithText(Res.string.Onboarding_ThingsToKnow_Title) + .isDisplayed() + } + clickOnText(Res.string.Onboarding_ThingsToKnow_Button) // Quiz - clickOnText("True") - clickOnText("True") + clickOnText(Res.string.Onboarding_PopQuiz_True) + clickOnText(Res.string.Onboarding_PopQuiz_True) - wait { onNodeWithText("Automated testing").isDisplayed() } + wait { onNodeWithText(Res.string.Onboarding_AutomatedTesting_Title).isDisplayed() } clickOnTag("No-AutoTest") - wait { onNodeWithText("Crash Reporting").isDisplayed() } + wait { onNodeWithText(Res.string.Onboarding_Crash_Title).isDisplayed() } clickOnTag("Yes-CrashReporting") if (dependencies.platformInfo.needsToRequestNotificationsPermission) { - wait { onNodeWithText("Get updates on internet censorship").isDisplayed() } + wait { + onNodeWithText(Res.string.Modal_EnableNotifications_Title).isDisplayed() + } clickOnTag("No-Notifications") } - wait { onNodeWithText("Default Settings").isDisplayed() } - clickOnText("Let’s go") + wait { onNodeWithText(Res.string.Onboarding_DefaultSettings_Title).isDisplayed() } + clickOnText(Res.string.Onboarding_DefaultSettings_Button_Go) - wait { onNodeWithContentDescription("OONI Probe").isDisplayed() } + wait { onNodeWithContentDescription(Res.string.app_name).isDisplayed() } } assertEquals( diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTest.kt index f475220d..78fb962f 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTest.kt @@ -5,21 +5,38 @@ 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.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeUp import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.test.runTest +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectNone +import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_RunFinished +import ooniprobe.composeapp.generated.resources.OONIRun_Run +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Test_Circumvention_Fullname +import ooniprobe.composeapp.generated.resources.Test_Experimental_Fullname +import ooniprobe.composeapp.generated.resources.Test_InstantMessaging_Fullname +import ooniprobe.composeapp.generated.resources.Test_Performance_Fullname +import ooniprobe.composeapp.generated.resources.Test_Psiphon_Fullname +import ooniprobe.composeapp.generated.resources.Test_Signal_Fullname +import ooniprobe.composeapp.generated.resources.measurement +import org.jetbrains.compose.resources.getPluralString import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.ooni.probe.config.OrganizationConfig +import org.ooni.probe.config.TestingFlags import org.ooni.probe.data.models.SettingsKey import org.ooni.probe.uitesting.helpers.checkLinkInsideWebView import org.ooni.probe.uitesting.helpers.checkSummaryInsideWebView import org.ooni.probe.uitesting.helpers.clickOnText +import org.ooni.probe.uitesting.helpers.isNewsMediaScan +import org.ooni.probe.uitesting.helpers.isOoni +import org.ooni.probe.uitesting.helpers.onNodeWithText import org.ooni.probe.uitesting.helpers.preferences import org.ooni.probe.uitesting.helpers.skipOnboarding import org.ooni.probe.uitesting.helpers.start @@ -34,6 +51,7 @@ class RunningTest { @Before fun setUp() = runTest { + TestingFlags.webviewJavascriptEnabled = true skipOnboarding() preferences.setValueByKey(SettingsKey.UPLOAD_RESULTS, true) start() @@ -42,21 +60,19 @@ class RunningTest { @Test fun signal() = runTest { + if (!isOoni) return@runTest with(compose) { - clickOnText("Run") + clickOnText(Res.string.OONIRun_Run) - clickOnText("Deselect all tests") - clickOnText("Signal Test") - clickOnText("Run 1 test") + clickOnText(Res.string.Dashboard_RunTests_SelectNone) + clickOnText(Res.string.Test_Signal_Fullname) + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) - wait(TEST_WAIT_TIMEOUT) { - onNodeWithText("Run finished. Tap to view results.").isDisplayed() - } - clickOnText("Run finished. Tap to view results.") + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) - clickOnText("Instant Messaging") - clickOnText("Signal Test") - wait { onNodeWithText("Measurement").isDisplayed() } + clickOnText(Res.string.Test_InstantMessaging_Fullname) + clickOnText(Res.string.Test_Signal_Fullname) + wait { onNodeWithText(Res.string.measurement).isDisplayed() } checkSummaryInsideWebView("Signal") } } @@ -64,21 +80,19 @@ class RunningTest { @Test fun psiphon() = runTest { + if (!isOoni) return@runTest with(compose) { - clickOnText("Run") + clickOnText(Res.string.OONIRun_Run) - clickOnText("Deselect all tests") - clickOnText("Psiphon Test") - clickOnText("Run 1 test") + clickOnText(Res.string.Dashboard_RunTests_SelectNone) + clickOnText(Res.string.Test_Psiphon_Fullname) + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) - wait(TEST_WAIT_TIMEOUT) { - onNodeWithText("Run finished. Tap to view results.").isDisplayed() - } - clickOnText("Run finished. Tap to view results.") + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) - clickOnText("Circumvention") - clickOnText("Psiphon Test") - wait { onNodeWithText("Measurement").isDisplayed() } + clickOnText(Res.string.Test_Circumvention_Fullname) + clickOnText(Res.string.Test_Psiphon_Fullname) + wait { onNodeWithText(Res.string.measurement).isDisplayed() } checkSummaryInsideWebView("Psiphon") } } @@ -86,24 +100,22 @@ class RunningTest { @Test fun httpHeader() = runTest { + if (!isOoni) return@runTest with(compose) { - clickOnText("Run") + clickOnText(Res.string.OONIRun_Run) - clickOnText("Deselect all tests") + clickOnText(Res.string.Dashboard_RunTests_SelectNone) onNodeWithTag("Run-DescriptorsList") .performScrollToNode(hasText("HTTP Header", substring = true)) .performTouchInput { swipeUp() } clickOnText("HTTP Header", substring = true) - clickOnText("Run 1 test") + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) - wait(TEST_WAIT_TIMEOUT) { - onNodeWithText("Run finished. Tap to view results.").isDisplayed() - } - clickOnText("Run finished. Tap to view results.") + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) - clickOnText("Performance") + clickOnText(Res.string.Test_Performance_Fullname) clickOnText("HTTP Header", substring = true) - wait { onNodeWithText("Measurement").isDisplayed() } + wait { onNodeWithText(Res.string.measurement).isDisplayed() } checkSummaryInsideWebView("middleboxes") } } @@ -111,25 +123,47 @@ class RunningTest { @Test fun stunReachability() = runTest { + if (!isOoni) return@runTest with(compose) { - clickOnText("Run") + clickOnText(Res.string.OONIRun_Run) - clickOnText("Deselect all tests") + clickOnText(Res.string.Dashboard_RunTests_SelectNone) onNodeWithTag("Run-DescriptorsList") .performScrollToNode(hasText("stunreachability")) .performTouchInput { swipeUp() } clickOnText("stunreachability", substring = true) - clickOnText("Run 1 test") + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) - wait(TEST_WAIT_TIMEOUT) { - onNodeWithText("Run finished. Tap to view results.").isDisplayed() - } - clickOnText("Run finished. Tap to view results.") + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) - clickOnText("Experimental") + clickOnText(Res.string.Test_Experimental_Fullname) compose.onAllNodesWithText("stunreachability")[0].performClick() - wait { onNodeWithText("Measurement").isDisplayed() } - checkLinkInsideWebView("https://ooni.org/nettest/http-requests/", "STUN Reachability") + wait { onNodeWithText(Res.string.measurement).isDisplayed() } + checkLinkInsideWebView( + "https://ooni.org/nettest/http-requests/", + "STUN Reachability" + ) + } + } + + + @Test + fun trustedInternationalMedia() = + runTest { + if (!isNewsMediaScan) return@runTest + with(compose) { + clickOnText(Res.string.OONIRun_Run) + + clickOnText(Res.string.Dashboard_RunTests_SelectNone) + clickOnText("Trusted International Media") + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) + + clickOnText("Trusted International Media") + clickOnText("https://www.dw.com") + wait { onNodeWithText(Res.string.measurement).isDisplayed() } + checkSummaryInsideWebView("https://www.dw.com") } } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt index 10a0e7e1..16cd017b 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt @@ -6,6 +6,27 @@ import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Name +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_Advanced_DebugLogs +import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_ChargingOnly +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports +import ooniprobe.composeapp.generated.resources.Settings_Proxy_Label +import ooniprobe.composeapp.generated.resources.Settings_Proxy_Psiphon +import ooniprobe.composeapp.generated.resources.Settings_Sharing_UploadResults +import ooniprobe.composeapp.generated.resources.Settings_TestOptions_Label +import ooniprobe.composeapp.generated.resources.Settings_Title +import ooniprobe.composeapp.generated.resources.Settings_WarmVPNInUse_Label +import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Description +import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Label +import ooniprobe.composeapp.generated.resources.back +import org.jetbrains.compose.resources.getString import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -39,9 +60,9 @@ class SettingsTest { assertTrue(preferences.getValueByKey(SettingsKey.NOTIFICATIONS_ENABLED).first() != true) with(compose) { - clickOnText("Settings") - clickOnText("Notifications") - clickOnText("Enabled") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_Notifications_Label) + clickOnText(Res.string.Settings_Notifications_Enabled) wait { preferences.getValueByKey(SettingsKey.NOTIFICATIONS_ENABLED).first() == true @@ -62,19 +83,19 @@ class SettingsTest { ) with(compose) { - clickOnText("Settings") - clickOnText("Test options") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_TestOptions_Label) - clickOnText("Automatically Publish Results") + clickOnText(Res.string.Settings_Sharing_UploadResults) wait { preferences.getValueByKey(SettingsKey.UPLOAD_RESULTS).first() == true } - clickOnText("Run tests automatically") + clickOnText(Res.string.Settings_AutomatedTesting_RunAutomatically) wait { preferences.getValueByKey(SettingsKey.AUTOMATED_TESTING_ENABLED).first() == true } - clickOnText("Only on WiFi") - clickOnText("Only while charging") + clickOnText(Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly) + clickOnText(Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly) wait { preferences.getValueByKey(SettingsKey.AUTOMATED_TESTING_WIFIONLY) @@ -95,16 +116,18 @@ class SettingsTest { ) with(compose) { - clickOnText("Settings") - clickOnText("Test options") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_TestOptions_Label) - onNodeWithText("0 categories enabled").assertIsDisplayed() - clickOnText("Website categories to test") + onNodeWithText(getString(Res.string.Settings_Websites_Categories_Description, 0)) + .assertIsDisplayed() + clickOnText(Res.string.Settings_Websites_Categories_Label) - clickOnText("Circumvention tools") - clickOnContentDescription("Back") + clickOnText(Res.string.CategoryCode_ANON_Name) + clickOnContentDescription(Res.string.back) - clickOnText("1 categories enabled") + onNodeWithText(getString(Res.string.Settings_Websites_Categories_Description, 1)) + .assertIsDisplayed() wait { preferences.getValueByKey(WebConnectivityCategory.ANON.settingsKey!!) @@ -123,9 +146,9 @@ class SettingsTest { ) with(compose) { - clickOnText("Settings") - clickOnText("Privacy") - clickOnText("Send crash reports") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_Privacy_Label) + clickOnText(Res.string.Settings_Privacy_SendCrashReports) wait { preferences.getValueByKey(SettingsKey.SEND_CRASH).first() == true @@ -143,9 +166,9 @@ class SettingsTest { ) with(compose) { - clickOnText("Settings") - clickOnText("OONI backend proxy") - clickOnText("Psiphon") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_Proxy_Label) + clickOnText(Res.string.Settings_Proxy_Psiphon) wait { preferences.getValueByKey(SettingsKey.PROXY_PROTOCOL) @@ -165,15 +188,14 @@ class SettingsTest { ) with(compose) { - clickOnText("Settings") - clickOnText("Advanced") - clickOnText("Debug logs") - clickOnText("Warn when VPN is in use") + clickOnText(Res.string.Settings_Title) + clickOnText(Res.string.Settings_Advanced_Label) - wait { - preferences.getValueByKey(SettingsKey.DEBUG_LOGS).first() == true && - preferences.getValueByKey(SettingsKey.WARN_VPN_IN_USE).first() == true - } + clickOnText(Res.string.Settings_Advanced_DebugLogs) + wait { preferences.getValueByKey(SettingsKey.DEBUG_LOGS).first() == true } + + clickOnText(Res.string.Settings_WarmVPNInUse_Label) + wait { preferences.getValueByKey(SettingsKey.WARN_VPN_IN_USE).first() == true } } } } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt index e7e084e7..4b1ab1dc 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt @@ -6,14 +6,30 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.test.runTest +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectNone +import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_RunFinished +import ooniprobe.composeapp.generated.resources.Modal_ResultsNotUploaded_Uploading +import ooniprobe.composeapp.generated.resources.OONIRun_Run +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Snackbar_ResultsSomeNotUploaded_UploadAll +import ooniprobe.composeapp.generated.resources.Test_InstantMessaging_Fullname +import ooniprobe.composeapp.generated.resources.Test_Signal_Fullname +import ooniprobe.composeapp.generated.resources.measurement +import org.jetbrains.compose.resources.getPluralString +import org.jetbrains.compose.resources.getString import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.ooni.probe.config.TestingFlags import org.ooni.probe.data.models.SettingsKey import org.ooni.probe.uitesting.helpers.checkSummaryInsideWebView import org.ooni.probe.uitesting.helpers.clickOnText +import org.ooni.probe.uitesting.helpers.isNewsMediaScan +import org.ooni.probe.uitesting.helpers.isOoni +import org.ooni.probe.uitesting.helpers.onNodeWithText import org.ooni.probe.uitesting.helpers.preferences import org.ooni.probe.uitesting.helpers.skipOnboarding import org.ooni.probe.uitesting.helpers.start @@ -29,6 +45,7 @@ class UploadResultTest { @Before fun setUp() = runTest { + TestingFlags.webviewJavascriptEnabled = true skipOnboarding() preferences.setValueByKey(SettingsKey.UPLOAD_RESULTS, false) start() @@ -38,37 +55,62 @@ class UploadResultTest { @Ignore("Single test upload is currently failing on the dev back-end") fun uploadSingleResult() = runTest { + if (!isOoni) return@runTest with(compose) { - clickOnText("Run") + clickOnText(Res.string.OONIRun_Run) - clickOnText("Deselect all tests") - clickOnText("Signal Test") - clickOnText("Run 1 test") + clickOnText(Res.string.Dashboard_RunTests_SelectNone) + clickOnText(Res.string.Test_Signal_Fullname) + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) - wait(TEST_WAIT_TIMEOUT) { - onNodeWithText("Run finished. Tap to view results.").isDisplayed() - } - clickOnText("Run finished. Tap to view results.") + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) - clickOnText("Instant Messaging") - clickOnText("Upload All") + clickOnText(Res.string.Test_InstantMessaging_Fullname) + clickOnText(Res.string.Snackbar_ResultsSomeNotUploaded_UploadAll) - wait { - onNodeWithText("Uploading", substring = true).isDisplayed() - } - waitUntil(10.seconds.inWholeMilliseconds) { - onNodeWithText("Uploading", substring = true).isNotDisplayed() - } + val onUploading = + onNodeWithText(getString(Res.string.Modal_ResultsNotUploaded_Uploading, 1)) + wait { onUploading.isDisplayed() } + wait(10.seconds) { onUploading.isNotDisplayed() } - Thread.sleep(10000) + Thread.sleep(5000) - clickOnText("Signal Test") + clickOnText(Res.string.Test_Signal_Fullname) - wait { onNodeWithText("Measurement").isDisplayed() } + wait { onNodeWithText(Res.string.measurement).isDisplayed() } checkSummaryInsideWebView("Signal") } } + @Test + fun uploadSingleResultNewsMediaScan() = + runTest { + if (!isNewsMediaScan) return@runTest + with(compose) { + clickOnText(Res.string.OONIRun_Run) + + clickOnText(Res.string.Dashboard_RunTests_SelectNone) + clickOnText("Trusted International Media") + clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + + clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) + + clickOnText("Trusted International Media") + clickOnText(Res.string.Snackbar_ResultsSomeNotUploaded_UploadAll) + + val onUploading = onNodeWithText("Uploading", substring = true) + wait { onUploading.isDisplayed() } + wait(60.seconds) { onUploading.isNotDisplayed() } + + Thread.sleep(5000) + + clickOnText("https://www.dw.com") + + wait { onNodeWithText(Res.string.measurement).isDisplayed() } + checkSummaryInsideWebView("https://www.dw.com") + } + } + companion object { private val TEST_WAIT_TIMEOUT = 1.minutes } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ApplicationTestHelper.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ApplicationTestHelper.kt index 2a252ac2..374c6aa0 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ApplicationTestHelper.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ApplicationTestHelper.kt @@ -7,6 +7,7 @@ import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.Dispatchers import org.ooni.probe.AndroidApplication import org.ooni.probe.MainActivity +import org.ooni.probe.config.OrganizationConfig val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext @@ -25,3 +26,9 @@ fun start(intent: Intent): ActivityScenario { dependencies.backgroundContext = Dispatchers.Unconfined return ActivityScenario.launch(intent) } + +val isOoni + get() = OrganizationConfig.baseSoftwareName.contains("ooni") + +val isNewsMediaScan + get() = OrganizationConfig.baseSoftwareName.contains("news") diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt index 43c99cfe..1c1eee3d 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt @@ -1,8 +1,10 @@ package org.ooni.probe.uitesting.helpers +import androidx.annotation.StringRes import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -12,19 +14,32 @@ import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.Locator +import co.touchlab.kermit.Logger import kotlinx.coroutines.runBlocking import org.hamcrest.CoreMatchers.containsString +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.getString import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +suspend fun ComposeTestRule.clickOnText( + stringRes: StringResource, + timeout: Duration = DEFAULT_WAIT_TIMEOUT, +): SemanticsNodeInteraction = + clickOnText(getString(stringRes), timeout = timeout) + fun ComposeTestRule.clickOnText( text: String, substring: Boolean = false, + timeout: Duration = DEFAULT_WAIT_TIMEOUT, ): SemanticsNodeInteraction { - wait { onNodeWithText(text, substring = substring).isDisplayed() } + wait(timeout) { onNodeWithText(text, substring = substring).isDisplayed() } return onNodeWithText(text, substring = substring).performClick() } +suspend fun ComposeTestRule.clickOnContentDescription(stringRes: StringResource)= + clickOnContentDescription(getString(stringRes)) + fun ComposeTestRule.clickOnContentDescription(contentDescription: String): SemanticsNodeInteraction { wait { onNodeWithContentDescription(contentDescription).isDisplayed() } return onNodeWithContentDescription(contentDescription).performClick() @@ -35,6 +50,15 @@ fun ComposeTestRule.clickOnTag(tag: String): SemanticsNodeInteraction { return onNodeWithTag(tag).performClick() } +suspend fun ComposeTestRule.onNodeWithText(stringRes: StringResource) = + onNodeWithText(getString(stringRes)) + +suspend fun ComposeTestRule.onAllNodesWithText(stringRes: StringResource) = + onAllNodesWithText(getString(stringRes)) + +suspend fun ComposeTestRule.onNodeWithContentDescription(stringRes: StringResource) = + onNodeWithContentDescription(getString(stringRes)) + fun ComposeTestRule.wait( timeout: Duration = DEFAULT_WAIT_TIMEOUT, check: suspend () -> Boolean, @@ -54,7 +78,8 @@ fun ComposeTestRule.waitAssertion( try { assertion() true - } catch (e: AssertionError) { + } catch (e: RuntimeException) { + Logger.w("waitAssertion failure", e) false } } @@ -83,4 +108,4 @@ fun ComposeTestRule.checkLinkInsideWebView( } } -private val WEBSITE_WAIT_TIMEOUT = 10.seconds +private val WEBSITE_WAIT_TIMEOUT = 20.seconds diff --git a/composeApp/src/commonMain/composeResources/drawable-hdpi/onboarding.webp b/composeApp/src/commonMain/composeResources/drawable-hdpi/onboarding.webp new file mode 100644 index 00000000..f1eef02b Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable-hdpi/onboarding.webp differ diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/config/TestingFlags.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/config/TestingFlags.kt new file mode 100644 index 00000000..c83a2fe0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/config/TestingFlags.kt @@ -0,0 +1,5 @@ +package org.ooni.probe.config + +object TestingFlags { + var webviewJavascriptEnabled = false +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt index 8974bba0..a823601a 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt @@ -36,6 +36,7 @@ import ooniprobe.composeapp.generated.resources.measurement import ooniprobe.composeapp.generated.resources.refresh import org.intellij.markdown.html.urlEncode import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.config.TestingFlags import org.ooni.probe.data.models.MeasurementModel import org.ooni.probe.ui.shared.TopBar @@ -50,7 +51,7 @@ fun MeasurementScreen( LaunchedEffect(url) { Logger.i("URL: $url") } val webViewState = rememberWebViewState(url) - webViewState.webSettings.isJavaScriptEnabled = false + webViewState.webSettings.isJavaScriptEnabled = TestingFlags.webviewJavascriptEnabled val webViewNavigator = rememberWebViewNavigator( // Don't allow other links to open requestInterceptor = object : RequestInterceptor { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt index 8648864f..00e8533d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.selection.triStateToggleable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog @@ -39,6 +41,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_Description @@ -266,12 +269,17 @@ private fun DescriptorItem( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() - .clickable { onDropdownToggled() }, + .padding(vertical = 4.dp) + .triStateToggleable( + state = descriptorItem.state, + onClick = { onChecked(descriptorItem.state != ToggleableState.On) }, + role = Role.Checkbox + ), ) { TriStateCheckbox( state = descriptorItem.state, - onClick = { onChecked(descriptorItem.state != ToggleableState.On) }, - modifier = Modifier.padding(end = 16.dp), + onClick = null, + modifier = Modifier.padding(start = 16.dp, end = 20.dp), ) TestDescriptorLabel(descriptor) IconButton(onClick = { onDropdownToggled() }) {