Browse Source

Generate screenshots for foreign languages ("de" and "fr" for now). #2454

pull/2449/head
Benoit Marty 3 months ago
parent
commit
609abee809
  1. 1
      .gitattributes
  2. 4
      .github/workflows/sync-localazy.yml
  3. 5
      build.gradle.kts
  4. 31
      tests/uitests/src/test/kotlin/ui/PreviewProvider.kt
  5. 120
      tests/uitests/src/test/kotlin/ui/S.kt
  6. 128
      tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt
  7. 52
      tests/uitests/src/test/kotlin/ui/T.kt
  8. 84
      tools/test/generateAllScreenshots.py

1
.gitattributes vendored

@ -1,3 +1,4 @@ @@ -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

4
.github/workflows/sync-localazy.yml

@ -22,7 +22,9 @@ jobs: @@ -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:

5
build.gradle.kts

@ -154,9 +154,14 @@ allprojects { @@ -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")
}
}
}

31
tests/uitests/src/test/kotlin/ui/PreviewProvider.kt

@ -0,0 +1,31 @@ @@ -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<TestPreview> {
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") }
}
}

120
tests/uitests/src/test/kotlin/ui/S.kt

@ -17,68 +17,16 @@ @@ -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<TestPreview> {
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 { @@ -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()
}
}

128
tests/uitests/src/test/kotlin/ui/ScreenshotTest.kt

@ -0,0 +1,128 @@ @@ -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()
}
}

52
tests/uitests/src/test/kotlin/ui/T.kt

@ -0,0 +1,52 @@ @@ -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,
)
}
}

84
tools/test/generateAllScreenshots.py

@ -0,0 +1,84 @@ @@ -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/<lang>"
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()
Loading…
Cancel
Save