diff --git a/.github/actions/generate-html-report/action.yml b/.github/actions/generate-html-report/action.yml new file mode 100644 index 000000000..19d7d4b60 --- /dev/null +++ b/.github/actions/generate-html-report/action.yml @@ -0,0 +1,7 @@ +name: 'Generate HTML Report' +description: 'Generates HTML report from screenshots' +runs: + using: "composite" + steps: + - run: ${GITHUB_ACTION_PATH}/scripts/generate-html-report.sh + shell: bash \ No newline at end of file diff --git a/.github/actions/generate-html-report/scripts/generate-html-report.sh b/.github/actions/generate-html-report/scripts/generate-html-report.sh new file mode 100755 index 000000000..8c39a2de9 --- /dev/null +++ b/.github/actions/generate-html-report/scripts/generate-html-report.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +mkdir reports +touch reports/report.html +cp */screenshots/*_compare.png reports/ +files=$(find . -type f -name "*_compare.png" | grep "reports/") +{ + echo '' + echo '' + echo '' + echo 'Screenshots failure report' + echo '' + echo '' + echo '' + echo '' + echo '' + echo '' + echo '' + echo '
' + echo '
' + echo '' + echo '' +} >> reports/report.html + + for file in $files; do + # Get the file name and insert newlines every 100 characters + fileName=$(basename "$file" | sed -r 's/(.{100})/\1
/g') + echo "" >> reports/report.html + echo "" >> reports/report.html + done +{ + echo '
File nameComparison
$(basename "$file")
' + echo '
' + echo '
' + echo '' + echo '' + echo '' +} >> reports/report.html + +echo "Report: " +cat reports/report.html diff --git a/.github/workflows/compare_screenshots.yml b/.github/workflows/compare_screenshots.yml new file mode 100644 index 000000000..aeb5dba0c --- /dev/null +++ b/.github/workflows/compare_screenshots.yml @@ -0,0 +1,49 @@ +name: Compare Screenshots + +on: + workflow_dispatch: + pull_request: + +jobs: + CompareScreenshots: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Verify Screenshots (roborazzi) + run: 'bash ./gradlew verifyRoborazziDebug' + + - id: generate-html + name: Generate Html Report + if: failure() + uses: ./.github/actions/generate-html-report + + - name: Generate screenshots tests reports tar.gz + if: failure() + run: | + tar cvzf mistica-screenshots-tests-report.tar.gz reports || echo "No screenshots tests reports found" + shell: bash + + - name: Checkout Telefonica/github-actions repo + if: failure() + uses: actions/checkout@v3 + with: + repository: Telefonica/github-actions + token: "${{ secrets.NOVUM_PRIVATE_REPOS }}" + path: .github/shared-actions + + - name: Upload reports to azure + if: failure() + uses: ./.github/shared-actions/azure/upload-to-storage + with: + azure-account-name: ${{secrets.AZURE_ACCOUNT_NAME}} + azure-account-key: ${{secrets.AZURE_ACCOUNT_KEY}} + globs: | + mistica-screenshots-tests-report.tar.gz + generate-summary: true diff --git a/.github/workflows/update_screenshot_baseline.yml b/.github/workflows/update_screenshot_baseline.yml new file mode 100644 index 000000000..500f96b23 --- /dev/null +++ b/.github/workflows/update_screenshot_baseline.yml @@ -0,0 +1,24 @@ +name: "Update screenshot baseline" +on: + workflow_dispatch: + +jobs: + screenshots_baseline: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Run Roborazzi Record + run: 'bash ./gradlew clean recordRoborazziDebug' + + - name: Check Git status + run: 'git status' + shell: bash + + - name: Commit and push screenshots baseline + id: commitAndPushScreenshotsBaseline + uses: EndBug/add-and-commit@v7 + with: + message: 'Updated screenshots baseline' + add: './**/screenshots/*' diff --git a/build.gradle b/build.gradle index 26d786926..1c2bc6323 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { accompanist_version = "0.30.1" coil_version = '2.2.2' constraintComposeVersion = '1.0.1' + roborazzi_version = "1.4.0" } repositories { google() @@ -27,6 +28,7 @@ buildscript { plugins { id 'org.jetbrains.kotlin.android' version '1.5.21' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false + id "io.github.takahirom.roborazzi" version '1.4.0' apply false } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f02a74898..b619500b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri May 19 13:07:06 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/build.gradle b/library/build.gradle index c9e35f5fa..ccf7740d3 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'maven-publish' + id 'io.github.takahirom.roborazzi' } android { @@ -51,6 +52,15 @@ android { sarifReport true checkDependencies true } + + testOptions { + unitTests { + includeAndroidResources = true + all { + systemProperty 'robolectric.graphicsMode', 'NATIVE' + } + } + } } task sourceJar(type: Jar) { @@ -88,7 +98,15 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" + testImplementation 'androidx.compose.ui:ui-test-manifest:1.0.5' + testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation "io.github.takahirom.roborazzi:roborazzi:$roborazzi_version" + testImplementation "io.github.takahirom.roborazzi:roborazzi-compose:$roborazzi_version" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" + testImplementation "androidx.test:rules:1.5.0" + testImplementation "androidx.test:core-ktx:1.5.0" + testImplementation 'androidx.test.ext:junit-ktx:1.1.5' + testImplementation 'androidx.test.espresso:espresso-core:3.2.0' debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" } diff --git a/library/screenshots/check_TextInput.png b/library/screenshots/check_TextInput.png new file mode 100644 index 000000000..443fc8a23 Binary files /dev/null and b/library/screenshots/check_TextInput.png differ diff --git a/library/screenshots/check_TextInputDisabled.png b/library/screenshots/check_TextInputDisabled.png new file mode 100644 index 000000000..6b342981f Binary files /dev/null and b/library/screenshots/check_TextInputDisabled.png differ diff --git a/library/screenshots/check_TextInputDisabled_dark.png b/library/screenshots/check_TextInputDisabled_dark.png new file mode 100644 index 000000000..6a0907d09 Binary files /dev/null and b/library/screenshots/check_TextInputDisabled_dark.png differ diff --git a/library/screenshots/check_TextInputWithError.png b/library/screenshots/check_TextInputWithError.png new file mode 100644 index 000000000..3d3a86dd2 Binary files /dev/null and b/library/screenshots/check_TextInputWithError.png differ diff --git a/library/screenshots/check_TextInputWithError_dark.png b/library/screenshots/check_TextInputWithError_dark.png new file mode 100644 index 000000000..648dc145f Binary files /dev/null and b/library/screenshots/check_TextInputWithError_dark.png differ diff --git a/library/screenshots/check_TextInput_dark.png b/library/screenshots/check_TextInput_dark.png new file mode 100644 index 000000000..353f090b0 Binary files /dev/null and b/library/screenshots/check_TextInput_dark.png differ diff --git a/library/screenshots/check_the_button_screenshot_BlauBrand.png b/library/screenshots/check_the_button_screenshot_BlauBrand.png new file mode 100644 index 000000000..3886ccc05 Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_BlauBrand.png differ diff --git a/library/screenshots/check_the_button_screenshot_BlauBrand_dark.png b/library/screenshots/check_the_button_screenshot_BlauBrand_dark.png new file mode 100644 index 000000000..6a5f0e69c Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_BlauBrand_dark.png differ diff --git a/library/screenshots/check_the_button_screenshot_MovistarBrand.png b/library/screenshots/check_the_button_screenshot_MovistarBrand.png new file mode 100644 index 000000000..ee97984bd Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_MovistarBrand.png differ diff --git a/library/screenshots/check_the_button_screenshot_MovistarBrand_dark.png b/library/screenshots/check_the_button_screenshot_MovistarBrand_dark.png new file mode 100644 index 000000000..8bb4ba2dd Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_MovistarBrand_dark.png differ diff --git a/library/screenshots/check_the_button_screenshot_O2Brand.png b/library/screenshots/check_the_button_screenshot_O2Brand.png new file mode 100644 index 000000000..6fab6c741 Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_O2Brand.png differ diff --git a/library/screenshots/check_the_button_screenshot_O2Brand_dark.png b/library/screenshots/check_the_button_screenshot_O2Brand_dark.png new file mode 100644 index 000000000..46769318e Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_O2Brand_dark.png differ diff --git a/library/screenshots/check_the_button_screenshot_TelefonicaBrand.png b/library/screenshots/check_the_button_screenshot_TelefonicaBrand.png new file mode 100644 index 000000000..16b0f073b Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_TelefonicaBrand.png differ diff --git a/library/screenshots/check_the_button_screenshot_TelefonicaBrand_dark.png b/library/screenshots/check_the_button_screenshot_TelefonicaBrand_dark.png new file mode 100644 index 000000000..2976d3f7b Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_TelefonicaBrand_dark.png differ diff --git a/library/screenshots/check_the_button_screenshot_VivoBrand.png b/library/screenshots/check_the_button_screenshot_VivoBrand.png new file mode 100644 index 000000000..c13a91227 Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_VivoBrand.png differ diff --git a/library/screenshots/check_the_button_screenshot_VivoBrand_dark.png b/library/screenshots/check_the_button_screenshot_VivoBrand_dark.png new file mode 100644 index 000000000..b6f80006b Binary files /dev/null and b/library/screenshots/check_the_button_screenshot_VivoBrand_dark.png differ diff --git a/library/screenshots/check_the_password_is_initially_not_visible.png b/library/screenshots/check_the_password_is_initially_not_visible.png new file mode 100644 index 000000000..caf321eb2 Binary files /dev/null and b/library/screenshots/check_the_password_is_initially_not_visible.png differ diff --git a/library/screenshots/check_the_password_is_not_visible_after_toggling_twice_the_visibility_button.png b/library/screenshots/check_the_password_is_not_visible_after_toggling_twice_the_visibility_button.png new file mode 100644 index 000000000..caf321eb2 Binary files /dev/null and b/library/screenshots/check_the_password_is_not_visible_after_toggling_twice_the_visibility_button.png differ diff --git a/library/screenshots/check_the_password_is_visible_after_the_visibility_button_is_clicked.png b/library/screenshots/check_the_password_is_visible_after_the_visibility_button_is_clicked.png new file mode 100644 index 000000000..7511f95c0 Binary files /dev/null and b/library/screenshots/check_the_password_is_visible_after_the_visibility_button_is_clicked.png differ diff --git a/library/src/test/AndroidManifest.xml b/library/src/test/AndroidManifest.xml new file mode 100644 index 000000000..c2944977c --- /dev/null +++ b/library/src/test/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/test/java/com/telefonica/mistica/DummyActivity.kt b/library/src/test/java/com/telefonica/mistica/DummyActivity.kt new file mode 100644 index 000000000..f84cb814d --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/DummyActivity.kt @@ -0,0 +1,13 @@ +package com.telefonica.mistica + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.telefonica.mistica.R + +class DummyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.test_dummy_activity) + } +} diff --git a/library/src/test/java/com/telefonica/mistica/compose/button/ButtonBehaviourTest.kt b/library/src/test/java/com/telefonica/mistica/compose/button/ButtonBehaviourTest.kt new file mode 100644 index 000000000..4deb68dae --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/compose/button/ButtonBehaviourTest.kt @@ -0,0 +1,64 @@ +package com.telefonica.mistica.compose.button + +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.dp +import com.telefonica.mistica.compose.theme.MisticaTheme +import com.telefonica.mistica.compose.theme.brand.MovistarBrand +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class ButtonBehaviourTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `check the button is clicked`() = test { + `given Button`() + + `when the button is clicked`() + + `then the onClickListener has been invoked`() + } + + private fun TestScope.`given Button`() { + `when Button`() + } + + private fun TestScope.`when Button`() { + composeTestRule.setContent { + MisticaTheme(brand = MovistarBrand) { + Button( + text = textValue, + onClickListener = onClickListener, + modifier = Modifier.padding(16.dp) + ) + } + } + } + + private fun TestScope.`when the button is clicked`() { + composeTestRule.onNodeWithText(textValue).performClick() + } + + private fun TestScope.`then the onClickListener has been invoked`() { + assertTrue(clicked) + } + + private fun test(block: TestScope.() -> Unit) { + TestScope().block() + } + + private class TestScope { + val textValue = "textValue" + var clicked = false + val onClickListener: () -> Unit = { clicked = true } + } +} \ No newline at end of file diff --git a/library/src/test/java/com/telefonica/mistica/compose/button/ButtonKtTest.kt b/library/src/test/java/com/telefonica/mistica/compose/button/ButtonKtTest.kt new file mode 100644 index 000000000..6f55080c4 --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/compose/button/ButtonKtTest.kt @@ -0,0 +1,68 @@ +package com.telefonica.mistica.compose.button + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import com.telefonica.mistica.compose.theme.MisticaTheme +import com.telefonica.mistica.compose.theme.brand.BlauBrand +import com.telefonica.mistica.compose.theme.brand.Brand +import com.telefonica.mistica.compose.theme.brand.MovistarBrand +import com.telefonica.mistica.compose.theme.brand.O2Brand +import com.telefonica.mistica.compose.theme.brand.TelefonicaBrand +import com.telefonica.mistica.compose.theme.brand.VivoBrand +import com.telefonica.mistica.testutils.ScreenshotsTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +@RunWith(ParameterizedRobolectricTestRunner::class) +internal class ButtonKtTest(private val brand: Brand, private val darkTheme: Boolean): ScreenshotsTest() { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `check the button screenshot`() { + `when Button`(brand, darkTheme) + + `then screenshot is OK`(brand, darkTheme) + } + + private fun `when Button`(brand: Brand = MovistarBrand, darkTheme: Boolean) { + composeTestRule.setContent { + MisticaTheme(brand = brand, darkTheme = darkTheme) { + Surface { + Button( + text = "textValue", + onClickListener = { }, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } + + private fun `then screenshot is OK`(brand: Brand, darkTheme: Boolean) { + compareScreenshot(composeTestRule.onRoot(), brand, darkTheme) + } + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "Input: {0}") + fun brands() = listOf( + arrayOf(MovistarBrand, false), + arrayOf(VivoBrand, false), + arrayOf(O2Brand, false), + arrayOf(BlauBrand, false), + arrayOf(TelefonicaBrand, false), + arrayOf(MovistarBrand, true), + arrayOf(VivoBrand, true), + arrayOf(O2Brand, true), + arrayOf(BlauBrand, true), + arrayOf(TelefonicaBrand, true), + ) + } +} diff --git a/library/src/test/java/com/telefonica/mistica/compose/input/PasswordInputKtTest.kt b/library/src/test/java/com/telefonica/mistica/compose/input/PasswordInputKtTest.kt new file mode 100644 index 000000000..579ad96e5 --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/compose/input/PasswordInputKtTest.kt @@ -0,0 +1,80 @@ +package com.telefonica.mistica.compose.input + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import com.telefonica.mistica.compose.theme.MisticaTheme +import com.telefonica.mistica.compose.theme.brand.MovistarBrand +import com.telefonica.mistica.testutils.ScreenshotsTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class PasswordInputKtTest: ScreenshotsTest() { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `check the password is initially not visible`() = test { + `when PasswordInput`() + + `then screenshot is OK`() + } + + @Test + fun `check the password is visible after the visibility button is clicked`() = test { + `given PasswordInput`() + + `when the visibility button is clicked`() + + `then screenshot is OK`() + } + + @Test + fun `check the password is not visible after toggling twice the visibility button`() = test { + `given PasswordInput`() + + `when the visibility button is clicked`(times = 2) + + `then screenshot is OK`() + } + + private fun TestScope.`given PasswordInput`() { + `when PasswordInput`() + } + + private fun TestScope.`when PasswordInput`() { + composeTestRule.setContent { + MisticaTheme(brand = MovistarBrand) { + PasswordInput( + value = textValue, + onValueChange = onValueChanged, + label = "label", + ) + } + } + } + + private fun `when the visibility button is clicked`(times: Int = 1) { + repeat(times) { + composeTestRule.onNodeWithTag(TextInputTestTags.PASSWORD_VISIBILITY_TOGGLE).performClick() + } + } + + private fun `then screenshot is OK`() { + compareScreenshot(composeTestRule.onRoot()) + } + + private fun test(block: TestScope.() -> Unit) { + TestScope().block() + } + + private class TestScope { + val textValue = "textValue" + var valueChanged = false + val onValueChanged: (String) -> Unit = { valueChanged = true } + } +} diff --git a/library/src/test/java/com/telefonica/mistica/input/TextInputTest.kt b/library/src/test/java/com/telefonica/mistica/input/TextInputTest.kt new file mode 100644 index 000000000..7454dedd2 --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/input/TextInputTest.kt @@ -0,0 +1,73 @@ +package com.telefonica.mistica.input + +import android.widget.FrameLayout +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.rules.activityScenarioRule +import com.telefonica.mistica.DummyActivity +import com.telefonica.mistica.R +import com.telefonica.mistica.testutils.ScreenshotsTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +internal class TextInputTest: ScreenshotsTest() { + @get:Rule + val rule = activityScenarioRule() + + @Test + fun `check TextInput`() { + checkTextInput() + } + + @Config(qualifiers = "+night") + @Test + fun `check TextInput dark`() { + checkTextInput() + } + + @Test + fun `check TextInputWithError`() { + checkTextInput { + error = "Whatever error" + } + } + + @Config(qualifiers = "+night") + @Test + fun `check TextInputWithError dark`() { + checkTextInput { + error = "Whatever error" + } + } + + @Test + fun `check TextInputDisabled`() { + checkTextInput { + isEnabled = false + } + } + + @Config(qualifiers = "+night") + @Test + fun `check TextInputDisabled dark`() { + checkTextInput { + isEnabled = false + } + } + + private fun checkTextInput(onTextInput: TextInput.() -> Unit = {}) { + rule.scenario.onActivity { activity -> + val wrapper: FrameLayout = activity.findViewById(R.id.dummy_activity_wrapper) + val textInput = TextInput(activity) + wrapper.addView(textInput) + textInput.text = "Hello android devs!" + textInput.onTextInput() + + compareScreenshot(onView(ViewMatchers.withId(R.id.dummy_activity_wrapper))) + } + } +} diff --git a/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotUtils.kt b/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotUtils.kt new file mode 100644 index 000000000..29b78f2ba --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotUtils.kt @@ -0,0 +1,20 @@ +package com.telefonica.mistica.testutils + +import com.telefonica.mistica.compose.theme.brand.Brand + +object ScreenshotUtils { + fun getScreenshotName(brand: Brand? = null, extra: String = ""): String { + return """screenshots/ + ${TestUtils.findRunningTestMethodName()} + ${brandName(brand, prepend = "_")}${extra}.png""" + .replace("\\s+".toRegex(), "") + } + + private fun brandName(brand: Brand?, prepend: String = "", append: String = ""): String { + return if (brand != null) { + "$prepend${brand::class.java.simpleName}$append" + } else { + "" + } + } +} diff --git a/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotsTest.kt b/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotsTest.kt new file mode 100644 index 000000000..826672af8 --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/testutils/ScreenshotsTest.kt @@ -0,0 +1,34 @@ +package com.telefonica.mistica.testutils + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.test.espresso.ViewInteraction +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.captureRoboImage +import com.telefonica.mistica.compose.theme.brand.Brand +import org.robolectric.annotation.Config + +@Config(sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel5) +open class ScreenshotsTest { + fun compareScreenshot( + node: SemanticsNodeInteraction, + brand: Brand? = null, + darkTheme: Boolean = false, + extra: String = "", + ) { + node.captureRoboImage(screenshotName(brand, darkTheme, extra)) + } + + fun compareScreenshot( + node: ViewInteraction, + brand: Brand? = null, + darkTheme: Boolean = false, + extra: String = "", + ) { + node.captureRoboImage(screenshotName(brand, darkTheme, extra)) + } + + private fun screenshotName(brand: Brand?, darkTheme: Boolean, extra: String) = ScreenshotUtils.getScreenshotName( + brand = brand, + extra = if (darkTheme) "_dark$extra" else extra + ) +} diff --git a/library/src/test/java/com/telefonica/mistica/testutils/TestUtils.kt b/library/src/test/java/com/telefonica/mistica/testutils/TestUtils.kt new file mode 100644 index 000000000..b0a24a910 --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/testutils/TestUtils.kt @@ -0,0 +1,24 @@ +package com.telefonica.mistica.testutils + +import org.junit.Test + +object TestUtils { + fun findRunningTestMethodName(): String { + val stackTrace = Thread.currentThread().stackTrace + for (element in stackTrace) { + val methodName = element.methodName + val className = element.className + val testClass = Class.forName(className) + val methods = testClass.declaredMethods + for (method in methods) { + if (method.name == methodName && method.isAnnotationPresent(Test::class.java)) { + return methodName.replace( + oldValue = " ", + newValue = "_" + ) + } + } + } + throw IllegalStateException("Could not find running test method name") + } +} \ No newline at end of file diff --git a/library/src/test/res/layout/test_dummy_activity.xml b/library/src/test/res/layout/test_dummy_activity.xml new file mode 100644 index 000000000..d032cf331 --- /dev/null +++ b/library/src/test/res/layout/test_dummy_activity.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/screenshots.md b/screenshots.md new file mode 100644 index 000000000..d3196f1f0 --- /dev/null +++ b/screenshots.md @@ -0,0 +1,42 @@ +## Screenshot testing +Screenshot tests compares current screenshots of the app with a set of desired ones. To achieve that we need to perform 2 tasks: + + 1. Generate and store a set of desired screenshots (also known as baseline). + These screenshots will be generated using a reference device to avoid differences based on Android devices/versions + 2. Take screenshots of the app with this reference device and compare them with the baseline. + Then generate reports with differences detected + +To perform these tasks we use [Roborazzi](https://github.com/takahirom/roborazzi) library which uses gradle tasks to generate baseline and store screenshots + +## Add a new screenshot +To create a new screenshot create a new unitTest with Roboelectric in the test folder of the module to test: + @RunWith(RobolectricTestRunner::class) +or + @RunWith(ParameterizedRobolectricTestRunner::class) +depending if it's a parametrized test or not. Use + @RunWith(Enclosed::class) +in case multiple of them must be on the same file. + +Then use `captureRoboImage()` function to take the screenshot and use `ScreenshotUtils.getScreenshotName()` to set the name of the test.: + + composeTestRule.onRoot() + .captureRoboImage(ScreenshotUtils.getScreenshotName()) + +For traditional views run an activity or check `TextInputTest.kt` example on how to use a dummy activity to load them inside. + +## Check that changes in the screen are detected +`Compare Screenshots` job will run in each pr and will run the `verifyRoborazziDebug` gradle task that checks the screenshots. In case this fails it will +generate a report and upload it to azure. + +## Access to screenshots test report +At the bottom of the summary of the action that has failed you'll see that a report has been uploaded to azure. + +Once that compressed file is downloaded an html report is there with the differences detected between the current screenshot (left side) and the +newly generated one (right side). + +## Update screenshots baseline +Either run the ci job `Update screenshot baseline` that will create the images and perform a commit for you or run the gradle task `recordRoborazziDebug` and +this way it allows you to check everything is ok before uploading the images. + + +