diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4823480f..21480be0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -32,6 +32,10 @@ jobs: recipe_uuid: ${{ vars.GMSAAS_RECIPE_UUID }} - name: Run tests run: bash scripts/run_tests.bash + - name: Run screenshot tests + run: | + bash scripts/run_screenshots.sh + zip -r screenshots.zip screenshots - name: Publish Unit Test Report uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails @@ -83,3 +87,10 @@ jobs: path: | ./app/build/outputs/apk/debug/app-debug.apk ./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + - name: Archive screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: | + ./screenshots.zip diff --git a/app/build.gradle b/app/build.gradle index 46a23698..de6815d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -245,6 +245,7 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version" androidTestImplementation "androidx.test.uiautomator:uiautomator:$androidx_test_uiautomator_version" androidTestImplementation "org.robolectric:annotations:$robolectric_version" + androidTestImplementation "com.google.testparameterinjector:test-parameter-injector:$test_parameter_injector_version" androidTestUtil "androidx.test:orchestrator:$androidx_test_orchestrator_version" diff --git a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/IntegrationTest.java b/app/src/androidTest/java/ca/rmen/android/poetassistant/main/IntegrationTest.java index 06eb65d1..fbc758c1 100644 --- a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/IntegrationTest.java +++ b/app/src/androidTest/java/ca/rmen/android/poetassistant/main/IntegrationTest.java @@ -213,7 +213,7 @@ public void copyCleanLayoutTest() { public void themeTest() { openMenuItem(R.string.action_settings); clickPreference(R.string.pref_theme_title); - onView(withText(R.string.pref_theme_value_light)).check(matches(isChecked())); + onView(withText(R.string.pref_theme_value_auto)).check(matches(isChecked())); onView(withText(R.string.pref_theme_value_dark)).perform(click()); pressBack(); diff --git a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/ScreenshotTest.java b/app/src/androidTest/java/ca/rmen/android/poetassistant/main/ScreenshotTest.java deleted file mode 100644 index e9451f89..00000000 --- a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/ScreenshotTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2017 Carmen Alvarez - * - * This file is part of Poet Assistant. - * - * Poet Assistant is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Poet Assistant is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Poet Assistant. If not, see . - */ - -package ca.rmen.android.poetassistant.main; - - -import android.Manifest; -import android.graphics.Bitmap; -import android.os.SystemClock; - -import org.junit.FixMethodOrder; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.MethodSorters; - -import java.io.IOException; -import java.util.HashSet; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.LargeTest; -import androidx.test.rule.GrantPermissionRule; -import androidx.test.runner.screenshot.BasicScreenCaptureProcessor; -import androidx.test.runner.screenshot.ScreenCapture; -import androidx.test.runner.screenshot.ScreenCaptureProcessor; -import ca.rmen.android.poetassistant.R; -import ca.rmen.android.poetassistant.main.rules.PoetAssistantActivityTestRule; - -import static androidx.test.espresso.Espresso.onIdle; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.pressBack; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static ca.rmen.android.poetassistant.main.TestAppUtils.search; -import static ca.rmen.android.poetassistant.main.TestAppUtils.starQueryWord; -import static ca.rmen.android.poetassistant.main.TestAppUtils.typePoem; -import static ca.rmen.android.poetassistant.main.TestUiUtils.clickPreference; -import static ca.rmen.android.poetassistant.main.TestUiUtils.openMenuItem; -import static ca.rmen.android.poetassistant.main.TestUiUtils.swipeViewPagerLeft; - -@LargeTest -@Ignore -@RunWith(AndroidJUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ScreenshotTest { - - @Rule - public PoetAssistantActivityTestRule mActivityTestRule = new PoetAssistantActivityTestRule<>(MainActivity.class, true); - - @Rule - public GrantPermissionRule writeScreenshotRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE); - - @Test - public void takeLightScreenshotsTest() { - takeScreenshots(); - } - - @Test - public void takeDarkScreenshotsTest() { - openMenuItem(R.string.action_settings); - clickPreference(R.string.pref_theme_title); - onView(withText(R.string.pref_theme_value_dark)).perform(click()); - pressBack(); - takeScreenshots(); - } - - private void takeScreenshots() { - starWords("acquiesce", "askance", "benight", "deferential", "fractious", "implacable", "obfuscation", "peon", "possibleness"); - search("chance"); - takeScreenshot("rhymer"); - swipeViewPagerLeft(1); - takeScreenshot("thesaurus"); - swipeViewPagerLeft(1); - takeScreenshot("dictionary"); - swipeViewPagerLeft(1); - typePoem("Roses are red.\nViolets are blue.\nIf you are a poet,\nthis app is for you."); - SystemClock.sleep(1000); - takeScreenshot("composer"); - swipeViewPagerLeft(1); - takeScreenshot("favorites"); - openMenuItem(R.string.action_settings); - takeScreenshot("settings"); - } - - private void starWords(String... words) { - for (String word : words) { - search(word); - onIdle(); - starQueryWord(); - } - } - - // https://stackoverflow.com/questions/38519568/how-to-take-screenshot-at-the-point-where-test-fail-in-espresso - private void takeScreenshot(String filename) { - SystemClock.sleep(500); // :( - onIdle(); - ScreenCapture capture = androidx.test.runner.screenshot.Screenshot.capture(); - capture.setName(filename); - capture.setFormat(Bitmap.CompressFormat.PNG); - - HashSet processors = new HashSet<>(); - processors.add(new BasicScreenCaptureProcessor()); - - try { - capture.process(processors); - } catch (IOException e) { - e.printStackTrace(); - } - } - - -} diff --git a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/TaskStackTest.java b/app/src/androidTest/java/ca/rmen/android/poetassistant/main/TaskStackTest.java index 0c57bb9f..8530e2a4 100644 --- a/app/src/androidTest/java/ca/rmen/android/poetassistant/main/TaskStackTest.java +++ b/app/src/androidTest/java/ca/rmen/android/poetassistant/main/TaskStackTest.java @@ -87,7 +87,7 @@ private void deepLinkAfterThemeChangeTest(String deepLinkUrl) { Intent intent = new Intent(); mActivityTestRule.launchActivity(intent); clickPreference(R.string.pref_theme_title); - onView(withText(R.string.pref_theme_value_light)).check(matches(isChecked())); + onView(withText(R.string.pref_theme_value_auto)).check(matches(isChecked())); onView(withText(R.string.pref_theme_value_dark)).perform(click()); // Open a deep link diff --git a/app/src/androidTest/kotlin/ca/rmen/android/poetassistant/main/ScreenshotTest.kt b/app/src/androidTest/kotlin/ca/rmen/android/poetassistant/main/ScreenshotTest.kt new file mode 100644 index 00000000..bb830712 --- /dev/null +++ b/app/src/androidTest/kotlin/ca/rmen/android/poetassistant/main/ScreenshotTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017 - present Carmen Alvarez + * + * This file is part of Poet Assistant. + * + * Poet Assistant is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Poet Assistant is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Poet Assistant. If not, see . + */ +package ca.rmen.android.poetassistant.main + +import android.app.Application +import android.graphics.Bitmap +import android.os.SystemClock +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.screenshot.ScreenCapture +import androidx.test.runner.screenshot.ScreenCaptureProcessor +import androidx.test.runner.screenshot.Screenshot +import ca.rmen.android.poetassistant.R +import ca.rmen.android.poetassistant.Theme.setThemeFromSettings +import ca.rmen.android.poetassistant.main.rules.PoetAssistantActivityTestRule +import ca.rmen.android.poetassistant.settings.SettingsPrefs +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.io.File +import java.io.IOException + + +@LargeTest +@RunWith(TestParameterInjector::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ScreenshotTest { + enum class ThemePreference(val value: String) { + LIGHT(SettingsPrefs.THEME_LIGHT), + DARK(SettingsPrefs.THEME_DARK) + } + + @TestParameter + lateinit var themePreference: ThemePreference + + private lateinit var deviceScreenshotsFolder: File + + @JvmField + @Rule + val activityTestRule: PoetAssistantActivityTestRule = PoetAssistantActivityTestRule( + MainActivity::class.java, true + ) + + @Before + fun setup() { + // Set the theme + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val settingsPrefs = + SettingsPrefs(ApplicationProvider.getApplicationContext()) + settingsPrefs.theme = themePreference.value + setThemeFromSettings(settingsPrefs) + } + + // Create a fresh folder to store the screenshots + deviceScreenshotsFolder = File( + ApplicationProvider.getApplicationContext().getExternalFilesDir(null), + "screenshots-${themePreference.value}" + ) + if (deviceScreenshotsFolder.exists()) { + deviceScreenshotsFolder.deleteRecursively() + } + deviceScreenshotsFolder.mkdirs() + } + + @Test + fun testTakeScreenshots() { + starWords( + "acquiesce", + "askance", + "benight", + "deferential", + "fractious", + "implacable", + "obfuscation", + "peon", + "possibleness" + ) + TestAppUtils.search("chance") + takeScreenshot("rhymer") + TestUiUtils.swipeViewPagerLeft(1) + takeScreenshot("thesaurus") + TestUiUtils.swipeViewPagerLeft(1) + takeScreenshot("dictionary") + TestUiUtils.swipeViewPagerLeft(1) + TestAppUtils.typePoem("Roses are red.\nViolets are blue.\nIf you are a poet,\nthis app is for you.") + SystemClock.sleep(1000) + takeScreenshot("composer") + TestUiUtils.swipeViewPagerLeft(1) + takeScreenshot("favorites") + TestUiUtils.openMenuItem(R.string.action_settings) + takeScreenshot("settings") + } + + private fun starWords(vararg words: String) { + for (word in words) { + TestAppUtils.search(word) + Espresso.onIdle() + TestAppUtils.starQueryWord() + } + } + + private inner class Processor : ScreenCaptureProcessor { + override fun process(capture: ScreenCapture): String { + val deviceFile = File(deviceScreenshotsFolder, "${capture.name}.png") + deviceFile.outputStream().use { + capture.bitmap.compress( + Bitmap.CompressFormat.PNG, + 0, + it, + ) + } + return deviceFile.absolutePath + } + } + + private val processor = Processor() + + // https://stackoverflow.com/questions/38519568/how-to-take-screenshot-at-the-point-where-test-fail-in-espresso + private fun takeScreenshot(filename: String) { + SystemClock.sleep(500) // :( + Espresso.onIdle() + val capture = Screenshot.capture() + capture.setName(filename) + capture.setFormat(Bitmap.CompressFormat.PNG) + + try { + capture.process(setOf(processor)) + } catch (e: IOException) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/kotlin/ca/rmen/android/poetassistant/settings/SettingsPrefs.kt b/app/src/main/kotlin/ca/rmen/android/poetassistant/settings/SettingsPrefs.kt index 087f5b97..90f9ed9e 100644 --- a/app/src/main/kotlin/ca/rmen/android/poetassistant/settings/SettingsPrefs.kt +++ b/app/src/main/kotlin/ca/rmen/android/poetassistant/settings/SettingsPrefs.kt @@ -129,7 +129,7 @@ class SettingsPrefs(application: Application) { var theme: String get() { - return sharedPreferences.getString(PREF_THEME, THEME_LIGHT) ?: THEME_LIGHT + return sharedPreferences.getString(PREF_THEME, THEME_LIGHT) ?: THEME_AUTO } set(newValue) { sharedPreferences.edit().putString(PREF_THEME, newValue).apply() diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 3a775da4..caf1e8f6 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -104,7 +104,7 @@ android:title="@string/pref_category_ui"> $output 2>&1 & -done - -# Some more manual steps: -# - retrieve the screenshots using the device explorer. They will be in /sdcard/Pictures/screenshots. The /sdcard folder may be found in /mnt. -# - Save them to the different folders (tablet/light, phone/dark, etc). -# - use the rename.sh script to rename the files to strip the uuids from the names. diff --git a/scripts/run_screenshots.sh b/scripts/run_screenshots.sh new file mode 100755 index 00000000..63d56402 --- /dev/null +++ b/scripts/run_screenshots.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +./gradlew assembleDebug assembleDebugAndroidTest +mapfile -t devices < <(adb devices | grep "device$" | sed -e 's/\t.*$//g') + +# Install the app and test apk to the devices. +for device in "${devices[@]}"; +do + echo "Installing apks to ${device}" + adb -s "${device}" install -r ./app/build/outputs/apk/debug/app-debug.apk + adb -s "${device}" install -r ./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk +done + +run_tests() { + device="$1" + output="/tmp/${device}.log" + echo "Running tests on ${device}, output to ${output}" + adb -s "${device}" shell settings put global animator_duration_scale 0 + adb -s "${device}" shell settings put global transition_animation_scale 0 + adb -s "${device}" shell settings put global window_animation_scale 0 + adb -s "${device}" shell am instrument -w -m -e debug false -e class ca.rmen.android.poetassistant.main.ScreenshotTest ca.rmen.android.poetassistant.test.test/androidx.test.runner.AndroidJUnitRunner > "${output}" 2>&1 + + echo "pulling screenshots to ${device}-light and ${device}-dark" + screenshots_local_folder="screenshots" + mkdir -p "${screenshots_local_folder}" + if [ -e "${screenshots_local_folder}/${device}-light" ] + then + rm -rf "${screenshots_local_folder}/${device}-light" + fi + if [ -e "${screenshots_local_folder}/${device}-dark" ] + then + rm -rf "${screenshots_local_folder}/${device}-dark" + fi + base_folder=/storage/emulated/0/Android/data/ca.rmen.android.poetassistant.test/files + adb -s "${device}" pull "${base_folder}/screenshots-Light" "${screenshots_local_folder}/${device}-light" + adb -s "${device}" pull "${base_folder}/screenshots-Dark" "${screenshots_local_folder}/${device}-dark" +} + +device_count=${#devices[@]} +for device in "${devices[@]}"; +do + if [ "${device_count}" = 1 ] + # Run blocking if there's only one device + then + run_tests "$device" + # Else spawn the test run so we can run multiple devices in parallel + else + run_tests "$device" & + fi +done + diff --git a/scripts/run_tests.bash b/scripts/run_tests.bash index 239371fd..f2b64040 100644 --- a/scripts/run_tests.bash +++ b/scripts/run_tests.bash @@ -4,4 +4,4 @@ adb shell settings put global animator_duration_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global window_animation_scale 0 -./gradlew --no-daemon testDebugUnitTest cAT jacocoTestReport +./gradlew --no-daemon testDebugUnitTest cAT jacocoTestReport -Pandroid.testInstrumentationRunnerArguments.notClass="ca.rmen.android.poetassistant.main.ScreenshotTest" \ No newline at end of file