From bdcd51548565d2e919d959ba356830343776cf1c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 28 Feb 2024 14:29:24 +0100 Subject: [PATCH] Generate screenshots for foreign languages ("de" and "fr" for now). #2454 --- .gitattributes | 1 + .github/workflows/sync-localazy.yml | 4 +- build.gradle.kts | 5 + .../src/test/kotlin/ui/PreviewProvider.kt | 31 +++++ tests/uitests/src/test/kotlin/ui/S.kt | 120 ++-------------- .../src/test/kotlin/ui/ScreenshotTest.kt | 128 ++++++++++++++++++ tests/uitests/src/test/kotlin/ui/T.kt | 52 +++++++ tools/test/generateAllScreenshots.py | 84 ++++++++++++ 8 files changed, 312 insertions(+), 113 deletions(-) create mode 100644 tests/uitests/src/test/kotlin/ui/PreviewProvider.kt create mode 100644 tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt create mode 100644 tests/uitests/src/test/kotlin/ui/T.kt create mode 100755 tools/test/generateAllScreenshots.py diff --git a/.gitattributes b/.gitattributes index d0cf036126..5f13ff4efb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text **/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 377bd3aa7a..035d44eba9 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -22,7 +22,9 @@ jobs: echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/localazy.gpg] https://maven.localazy.com/repository/apt/ stable main" | sudo tee /etc/apt/sources.list.d/localazy.list sudo apt-get update && sudo apt-get install localazy - name: Run Localazy script - run: ./tools/localazy/downloadStrings.sh --all + run: | + ./tools/localazy/downloadStrings.sh --all + ./tools/test/generateAllScreenshots.py - name: Create Pull Request for Strings uses: peter-evans/create-pull-request@v6 with: diff --git a/build.gradle.kts b/build.gradle.kts index ce46ed2ec4..1efc049df6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -154,9 +154,14 @@ allprojects { if (isScreenshotTest) { // Increase heap size for screenshot tests maxHeapSize = "2g" + // Record all the languages? + if (project.hasProperty("allLanguages").not()) { + exclude("ui/T.class") + } } else { // Disable screenshot tests by default exclude("ui/S.class") + exclude("ui/T.class") } } } diff --git a/tests/uitests/src/test/kotlin/ui/PreviewProvider.kt b/tests/uitests/src/test/kotlin/ui/PreviewProvider.kt new file mode 100644 index 0000000000..6f972c8116 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/PreviewProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ui + +import com.airbnb.android.showkase.models.Showkase +import com.google.testing.junit.testparameterinjector.TestParameter + +object PreviewProvider : TestParameter.TestParameterValuesProvider { + override fun provideValues(): List { + val metadata = Showkase.getMetadata() + val components = metadata.componentList.map(::ComponentTestPreview) + val colors = metadata.colorList.map(::ColorTestPreview) + val typography = metadata.typographyList.map(::TypographyTestPreview) + + return (components + colors + typography).filter { !it.toString().contains("compound") } + } +} diff --git a/tests/uitests/src/test/kotlin/ui/S.kt b/tests/uitests/src/test/kotlin/ui/S.kt index 0531adad35..a29ac963cd 100644 --- a/tests/uitests/src/test/kotlin/ui/S.kt +++ b/tests/uitests/src/test/kotlin/ui/S.kt @@ -17,68 +17,16 @@ package ui -import android.content.res.Configuration -import android.os.LocaleList -import androidx.activity.OnBackPressedDispatcher -import androidx.activity.OnBackPressedDispatcherOwner -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.unit.Density -import androidx.lifecycle.Lifecycle -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.detectEnvironment -import com.airbnb.android.showkase.models.Showkase -import com.android.ide.common.rendering.api.SessionParams -import com.android.resources.NightMode import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector -import io.element.android.compound.theme.ElementTheme -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.Locale /** - * BMA: Inspired from https://github.com/airbnb/Showkase/blob/master/showkase-screenshot-testing-paparazzi-sample/src/test/java/com/airbnb/android/showkase/screenshot/testing/paparazzi/sample/PaparazziSampleScreenshotTest.kt - * - * Credit to Alex Vanyo for creating this sample in the Now In Android app by Google. - * PR here - https://github.com/android/nowinandroid/pull/101. Modified the test from that PR to - * my own needs for this sample. - * - * *Note*: keep the class name as short as possible to get shorter filename for generated screenshot. - * Long name was ScreenshotTest. + * Screenshot test for the English version only. */ @RunWith(TestParameterInjector::class) -class S { - object PreviewProvider : TestParameter.TestParameterValuesProvider { - override fun provideValues(): List { - val metadata = Showkase.getMetadata() - val components = metadata.componentList.map(::ComponentTestPreview) - val colors = metadata.colorList.map(::ColorTestPreview) - val typography = metadata.typographyList.map(::TypographyTestPreview) - - return (components + colors + typography).filter { !it.toString().contains("compound") } - } - } - - @get:Rule - val paparazzi = Paparazzi( - environment = detectEnvironment().run { - // Workaround to work with API 34 (https://github.com/cashapp/paparazzi/issues/1025) - copy(compileSdkVersion = 33, platformDir = platformDir.replace("34", "33")) - }, - maxPercentDifference = 0.01, - renderingMode = SessionParams.RenderingMode.NORMAL, - ) - +class S : ScreenshotTest() { /** * *Note*: keep the method name as short as possible to get shorter filename for generated screenshot. * Long name was preview_test. @@ -87,67 +35,15 @@ class S { fun t( @TestParameter(valuesProvider = PreviewProvider::class) componentTestPreview: TestPreview, @TestParameter baseDeviceConfig: BaseDeviceConfig, - // @TestParameter(value = ["1.0", "1.5"]) fontScale: Float, @TestParameter(value = ["1.0"]) fontScale: Float, - // @TestParameter(value = ["en" "fr", "de", "ru"]) localeStr: String, + // Need to keep the TestParameter to have filename including the language. @TestParameter(value = ["en"]) localeStr: String, ) { - val locale = localeStr.toLocale() - Locale.setDefault(locale) // Needed for regional settings, as first day of week - val densityScale = baseDeviceConfig.deviceConfig.density.dpiValue / 160f - val customScreenHeight = componentTestPreview.customHeightDp()?.value?.let { it * densityScale }?.toInt() - paparazzi.unsafeUpdateConfig( - deviceConfig = baseDeviceConfig.deviceConfig.copy( - softButtons = false, - locale = localeStr, - nightMode = componentTestPreview.isNightMode().let { - when (it) { - true -> NightMode.NIGHT - false -> NightMode.NOTNIGHT - } - }, - screenHeight = customScreenHeight ?: baseDeviceConfig.deviceConfig.screenHeight, - ), + doTest( + componentTestPreview = componentTestPreview, + baseDeviceConfig = baseDeviceConfig, + fontScale = fontScale, + localeStr = localeStr, ) - paparazzi.snapshot { - val lifecycleOwner = LocalLifecycleOwner.current - CompositionLocalProvider( - LocalInspectionMode provides true, - LocalDensity provides Density( - density = LocalDensity.current.density, - fontScale = fontScale - ), - LocalConfiguration provides Configuration().apply { - setLocales(LocaleList(locale)) - uiMode = when (componentTestPreview.isNightMode()) { - true -> Configuration.UI_MODE_NIGHT_YES - false -> Configuration.UI_MODE_NIGHT_NO - } - }, - // Needed so that UI that uses it don't crash during screenshot tests - LocalOnBackPressedDispatcherOwner provides object : OnBackPressedDispatcherOwner { - override val lifecycle: Lifecycle get() = lifecycleOwner.lifecycle - override val onBackPressedDispatcher: OnBackPressedDispatcher get() = OnBackPressedDispatcher() - } - ) { - ElementTheme { - Box( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - ) { - componentTestPreview.Content() - } - } - } - } - } -} - -private fun String.toLocale(): Locale { - return when (this) { - "en" -> Locale.US - "fr" -> Locale.FRANCE - "de" -> Locale.GERMAN - else -> Locale.Builder().setLanguage(this).build() } } diff --git a/tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt new file mode 100644 index 0000000000..5c86e974f1 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ui + +import android.content.res.Configuration +import android.os.LocaleList +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.Density +import androidx.lifecycle.Lifecycle +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.detectEnvironment +import com.android.ide.common.rendering.api.SessionParams +import com.android.resources.NightMode +import io.element.android.compound.theme.ElementTheme +import org.junit.Rule +import java.util.Locale + +/** + * BMA: Inspired from https://github.com/airbnb/Showkase/blob/master/showkase-screenshot-testing-paparazzi-sample/src/test/java/com/airbnb/android/showkase/screenshot/testing/paparazzi/sample/PaparazziSampleScreenshotTest.kt + * + * Credit to Alex Vanyo for creating this sample in the Now In Android app by Google. + * PR here - https://github.com/android/nowinandroid/pull/101. Modified the test from that PR to + * my own needs for this sample. + * + * *Note*: keep the class name as short as possible to get shorter filename for generated screenshot. + * Long name was ScreenshotTest. + */ +open class ScreenshotTest { + @get:Rule + val paparazzi = Paparazzi( + environment = detectEnvironment().run { + // Workaround to work with API 34 (https://github.com/cashapp/paparazzi/issues/1025) + copy(compileSdkVersion = 33, platformDir = platformDir.replace("34", "33")) + }, + maxPercentDifference = 0.01, + renderingMode = SessionParams.RenderingMode.NORMAL, + ) + + protected fun doTest( + componentTestPreview: TestPreview, + baseDeviceConfig: BaseDeviceConfig, + fontScale: Float, + localeStr: String, + ) { + val locale = localeStr.toLocale() + Locale.setDefault(locale) // Needed for regional settings, as first day of week + val densityScale = baseDeviceConfig.deviceConfig.density.dpiValue / 160f + val customScreenHeight = componentTestPreview.customHeightDp()?.value?.let { it * densityScale }?.toInt() + paparazzi.unsafeUpdateConfig( + deviceConfig = baseDeviceConfig.deviceConfig.copy( + softButtons = false, + locale = localeStr, + nightMode = componentTestPreview.isNightMode().let { + when (it) { + true -> NightMode.NIGHT + false -> NightMode.NOTNIGHT + } + }, + screenHeight = customScreenHeight ?: baseDeviceConfig.deviceConfig.screenHeight, + ), + ) + paparazzi.snapshot { + val lifecycleOwner = LocalLifecycleOwner.current + CompositionLocalProvider( + LocalInspectionMode provides true, + LocalDensity provides Density( + density = LocalDensity.current.density, + fontScale = fontScale + ), + LocalConfiguration provides Configuration().apply { + setLocales(LocaleList(locale)) + uiMode = when (componentTestPreview.isNightMode()) { + true -> Configuration.UI_MODE_NIGHT_YES + false -> Configuration.UI_MODE_NIGHT_NO + } + }, + // Needed so that UI that uses it don't crash during screenshot tests + LocalOnBackPressedDispatcherOwner provides object : OnBackPressedDispatcherOwner { + override val lifecycle: Lifecycle get() = lifecycleOwner.lifecycle + override val onBackPressedDispatcher: OnBackPressedDispatcher get() = OnBackPressedDispatcher() + } + ) { + ElementTheme { + Box( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) { + componentTestPreview.Content() + } + } + } + } + } +} + +private fun String.toLocale(): Locale { + return when (this) { + "en" -> Locale.US + "fr" -> Locale.FRANCE + "de" -> Locale.GERMAN + else -> Locale.Builder().setLanguage(this).build() + } +} diff --git a/tests/uitests/src/test/kotlin/ui/T.kt b/tests/uitests/src/test/kotlin/ui/T.kt new file mode 100644 index 0000000000..babe9e9c69 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/T.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ui + +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Screenshot test for the Locale other then English. + */ +@RunWith(TestParameterInjector::class) +class T : ScreenshotTest() { + /** + * *Note*: keep the method name as short as possible to get shorter filename for generated screenshot. + * Long name was preview_test. + */ + @SuppressWarnings("MemberNameEqualsClassName") + @Test + fun t( + @TestParameter(valuesProvider = PreviewProvider::class) componentTestPreview: TestPreview, + @TestParameter baseDeviceConfig: BaseDeviceConfig, + @TestParameter(value = ["1.0"]) fontScale: Float, + @TestParameter(value = ["fr", "de"]) localeStr: String, + ) { + // Only test ComponentTestPreview, and only with the light theme + if (componentTestPreview.isNightMode() || componentTestPreview !is ComponentTestPreview) { + return + } + doTest( + componentTestPreview = componentTestPreview, + baseDeviceConfig = baseDeviceConfig, + fontScale = fontScale, + localeStr = localeStr, + ) + } +} diff --git a/tools/test/generateAllScreenshots.py b/tools/test/generateAllScreenshots.py new file mode 100755 index 0000000000..370f0a5597 --- /dev/null +++ b/tools/test/generateAllScreenshots.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import os +import re + + +def deleteExistingScreenshots(): + print("Deleting existing screenshots...") + os.system("rm -rf screenshots") + + +def generateAllScreenshots(): + print("Generating all screenshots...") + os.system("./gradlew recordPaparazziDebug -PallLanguages") + + +def detectLanguages(): + __doc__ = "Detect languages from screenshots, other than English" + files = os.listdir("tests/uitests/src/test/snapshots/images/") + languages = set(map(lambda file: file[-7:-5], files)) + languages = [lang for lang in languages if re.match("[a-z]", lang) and lang != "en"] + print("Detected languages: %s" % languages) + return languages + + +def compare(file1, file2): + __doc__ = "Compare two files, return True if different, False if identical." + # Compare file size + file1_stats = os.stat(file1) + file2_stats = os.stat(file2) + if file1_stats.st_size != file2_stats.st_size: + return True + # Compare file content + with open(file1, "rb") as f1, open(file2, "rb") as f2: + content1 = f1.read() + content2 = f2.read() + return content1 != content2 + + +def deleteDuplicatedScreenshots(lang): + __doc__ = "Delete screenshots identical to the English version for a language" + print("Deleting screenshots identical to the English version for language %s..." % lang) + files = os.listdir("tests/uitests/src/test/snapshots/images/") + # Filter files by language + files = [file for file in files if file[-7:-5] == lang] + identicalFileCounter = 0 + differentFileCounter = 0 + for file in files: + englishFile = file[:3] + "S" + file[4:-7] + "en" + file[-5:] + fullFile = "tests/uitests/src/test/snapshots/images/" + file + fullEnglishFile = "tests/uitests/src/test/snapshots/images/" + englishFile + isDifferent = compare(fullFile, fullEnglishFile) + if isDifferent: + differentFileCounter += 1 + else: + identicalFileCounter += 1 + os.remove(fullFile) + print("For language %s, keeping %d files and deleting %d files." % (lang, differentFileCounter, identicalFileCounter)) + + +def moveScreenshots(lang): + __doc__ = "Move screenshots to the folder per language" + targetFolder = "screenshots/" + lang + print("Moving screenshots for %s to %s..." % (lang, targetFolder)) + files = os.listdir("tests/uitests/src/test/snapshots/images/") + # Filter files by language + files = [file for file in files if file[-7:-5] == lang] + # Create the folder "./screenshots/" + os.makedirs(targetFolder, exist_ok=True) + for file in files: + fullFile = "tests/uitests/src/test/snapshots/images/" + file + os.rename(fullFile, targetFolder + "/" + file) + + +def main(): + deleteExistingScreenshots() + generateAllScreenshots() + lang = detectLanguages() + for l in lang: + deleteDuplicatedScreenshots(l) + moveScreenshots(l) + + +main()