From 1d7e7e19d9ac9168c66914729dfdf268f1d8a842 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 23 Aug 2025 16:43:50 +0200 Subject: [PATCH 1/3] Test previews with a11y details and add a first example with HomeViewA11yPreview. --- .../android/features/home/impl/HomeView.kt | 20 +++++++++ .../tests/konsist/KonsistPreviewTest.kt | 21 ++++++++++ .../kotlin/base/ComposablePreviewProvider.kt | 13 ++++++ .../src/test/kotlin/base/ScreenshotTest.kt | 11 ++++- .../src/test/kotlin/ui/PreviewA11yTest.kt | 42 +++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index c0e999e7f5..a0468aa32d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.hazeEffect @@ -313,3 +314,22 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: leaveRoomView = {} ) } + +@Preview +@Composable +internal fun HomeViewA11yPreview() = ElementPreview { + HomeView( + homeState = aHomeState(), + onRoomClick = {}, + onSettingsClick = {}, + onSetUpRecoveryClick = {}, + onConfirmRecoveryKeyClick = {}, + onStartChatClick = {}, + onRoomSettingsClick = {}, + onReportRoomClick = {}, + onMenuActionClick = {}, + onDeclineInviteAndBlockUser = {}, + acceptDeclineInviteView = {}, + leaveRoomView = {} + ) +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index fcab7b4f81..4c5863d1b9 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf import com.lemonappdev.konsist.api.ext.list.withName +import com.lemonappdev.konsist.api.ext.list.withNameEndingWith import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.verify.assertEmpty import com.lemonappdev.konsist.api.verify.assertTrue @@ -32,6 +33,26 @@ class KonsistPreviewTest { } } + @Test + fun `Check functions with 'A11yPreview'`() { + Konsist + .scopeFromProject() + .functions() + .withNameEndingWith("A11yPreview") + .assertTrue( + additionalMessage = "Functions with 'A11yPreview' suffix should have '@Previews' annotation and not '@PreviewsDayNight'," + + " should contain 'ElementPreview' composable," + + " should contain the tested view" + + " and should be internal" + ) { + val testedView = it.name.removeSuffix("A11yPreview") + it.text.contains("$testedView(") && + it.hasAllAnnotationsOf(PreviewsDayNight::class).not() && + it.text.contains("ElementPreview") && + it.hasInternalModifier + } + } + @Test fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() { Konsist diff --git a/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt b/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt index c4dea95296..477e6a2da2 100644 --- a/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt +++ b/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt @@ -29,6 +29,7 @@ object ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { AndroidComposablePreviewScanner() .scanPackageTrees(*PACKAGE_TREES) .getPreviews() + .filter { composablePreview -> composablePreview.methodName.endsWith("A11yPreview").not() } .withIndex() .toList() } @@ -36,6 +37,18 @@ object ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { override fun provideValues(): List>> = values } +object ComposableA11yPreviewProvider : TestParameter.TestParameterValuesProvider { + private val values: List> by lazy { + AndroidComposablePreviewScanner() + .scanPackageTrees(*PACKAGE_TREES) + .getPreviews() + .filter { composablePreview -> composablePreview.methodName.endsWith("A11yPreview") } + .toList() + } + + override fun provideValues(): List> = values +} + object Shard1ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { override fun provideValues(): List> = ComposablePreviewProvider.provideValues().filter { it.index % 4 == 0 }.map { it.value } diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index 02ffa418a3..114b2775f0 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.Density import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.RenderExtension import app.cash.paparazzi.TestName import com.android.resources.NightMode import io.element.android.compound.theme.ElementTheme @@ -112,7 +113,12 @@ fun createScreenshotIdFor(preview: ComposablePreview) = buil }.joinToString("_") object PaparazziPreviewRule { - fun createFor(preview: ComposablePreview, locale: String, deviceConfig: DeviceConfig = ScreenshotTest.defaultDeviceConfig): Paparazzi { + fun createFor( + preview: ComposablePreview, + locale: String, + deviceConfig: DeviceConfig = ScreenshotTest.defaultDeviceConfig, + renderExtensions: Set = setOf(), + ): Paparazzi { val densityScale = deviceConfig.density.dpiValue / 160f val customScreenHeight = preview.previewInfo.heightDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() return Paparazzi( @@ -125,7 +131,8 @@ object PaparazziPreviewRule { softButtons = false, screenHeight = customScreenHeight ?: deviceConfig.screenHeight, ), - maxPercentDifference = 0.01 + maxPercentDifference = 0.01, + renderExtensions = renderExtensions, ) } } diff --git a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt new file mode 100644 index 0000000000..d742db08b2 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package ui + +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import base.ComposableA11yPreviewProvider +import base.PaparazziPreviewRule +import base.ScreenshotTest +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + +/** + * Test that takes a preview and runs a screenshot test on it. + * It uses [ComposableA11yPreviewProvider] to test only previews that ends with "A11yPreview". + */ +@RunWith(TestParameterInjector::class) +class PreviewA11yTest( + @TestParameter(valuesProvider = ComposableA11yPreviewProvider::class) + val preview: ComposablePreview, +) { + @get:Rule + val paparazziRule = PaparazziPreviewRule.createFor( + preview = preview, + locale = "en", + renderExtensions = setOf(AccessibilityRenderExtension()), + ) + + @Test + fun snapshot() { + ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") + } +} From 27c242296e4331303878ec870ecc89946ecb225b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 25 Aug 2025 09:29:00 +0000 Subject: [PATCH 2/3] Update screenshots --- .../snapshots/images/features.home.impl_HomeViewA11y_en.png | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png new file mode 100644 index 0000000000..f34d4bae74 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21c09cac237b2b4823dbb945161c6d6d574dc2c8ffb37088696d09736d8e782a +size 124636 From cbb55fe1ae4f7a1a55ae98964b74972a01352fd4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Aug 2025 11:58:32 +0200 Subject: [PATCH 3/3] Fix typo in annotation name. --- .../io/element/android/tests/konsist/KonsistPreviewTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 4c5863d1b9..d0408fb61a 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -40,10 +40,10 @@ class KonsistPreviewTest { .functions() .withNameEndingWith("A11yPreview") .assertTrue( - additionalMessage = "Functions with 'A11yPreview' suffix should have '@Previews' annotation and not '@PreviewsDayNight'," + + additionalMessage = "Functions with 'A11yPreview' suffix should have '@Preview' annotation and not '@PreviewsDayNight'," + " should contain 'ElementPreview' composable," + " should contain the tested view" + - " and should be internal" + " and should be internal." ) { val testedView = it.name.removeSuffix("A11yPreview") it.text.contains("$testedView(") &&