diff --git a/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_float.png b/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_float.png index 1863cd2a29..b84c408333 100644 --- a/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_float.png +++ b/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_float.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc7c41bca5cc749f5327476182395c7549775b41c174288b8a276fe06150b7f -size 31345 +oid sha256:7602972205024ea3b3a56cde38949ec7679565a5ea2a77e14b1ec4fb23268c42 +size 31327 diff --git a/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_int.png b/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_int.png index 103aa986dd..5e4793775d 100644 --- a/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_int.png +++ b/compose-material/src/test/snapshots/images/com.google.android.horologist.compose.material_StepperA11yTest_int.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5179bd6215f87a0ad04bc093ee759e92a104d03639bf4a743fabd11af6c0156 -size 31207 +oid sha256:9425db9cf8c2e6b388bc9c2f19d9ca3e6f596b7eccf94180f8fa580e26f914e7 +size 31192 diff --git a/datalayer/phone-ui/build.gradle.kts b/datalayer/phone-ui/build.gradle.kts index 3161c65ae4..133747adde 100644 --- a/datalayer/phone-ui/build.gradle.kts +++ b/datalayer/phone-ui/build.gradle.kts @@ -106,6 +106,8 @@ dependencies { implementation(libs.compose.material3) implementation(libs.material) implementation(platform(libs.compose.bom)) + implementation(libs.compose.material.iconscore) + implementation(libs.compose.material.iconsext) testImplementation(libs.junit) diff --git a/datalayer/watch/api/current.api b/datalayer/watch/api/current.api index a83cf5ee87..56e7154b2e 100644 --- a/datalayer/watch/api/current.api +++ b/datalayer/watch/api/current.api @@ -13,7 +13,7 @@ package com.google.android.horologist.datalayer.watch { method public suspend Object? markSetupNoLongerComplete(kotlin.coroutines.Continuation); method @Deprecated public suspend Object? markTileAsInstalled(String tileName, kotlin.coroutines.Continuation); method @Deprecated public suspend Object? markTileAsRemoved(String tileName, kotlin.coroutines.Continuation); - method @CheckResult public suspend Object? startCompanion(String nodeId, kotlin.coroutines.Continuation); + method @CheckResult public suspend Object? startCompanion(String nodeId, kotlin.coroutines.Continuation); method public suspend Object? updateInstalledTiles(kotlin.coroutines.Continuation); property public kotlinx.coroutines.flow.Flow> connectedAndInstalledNodes; property public final kotlinx.coroutines.flow.Flow surfacesInfo; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad81acb646..150dd2f700 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] accompanist = "0.36.0" androidx-benchmark = "1.3.3" +accessibilityTestFramework = "4.1.1" androidx-complications-data = "1.2.1" androidx-concurrent = "1.2.0" androidx-constraintlayout-compose = "1.1.0" @@ -13,8 +14,8 @@ androidx-test-ext = "1.2.1" androidx-test-runner = "1.6.2" androidx-wear-watchface = "1.2.1" androidxActivity = "1.9.3" -androidxComposeBom = "2024.09.00-alpha" -androidxCore = "1.15.0" +androidxComposeBom = "2024.10.00" +androidxCore = "1.13.1" androidxLifecycle = "2.8.7" androidxNavigation = "2.8.3" androidxPhoneInteractions = "1.1.0-alpha04" @@ -65,6 +66,7 @@ wearToolingPreview = "1.0.0" wearcompose = "1.5.0-alpha05" [libraries] +accessibility-test-framework = { module = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", version.ref = "accessibilityTestFramework" } accompanist-testharness = { module = "com.google.accompanist:accompanist-testharness", version.ref = "accompanist" } android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" } androidx-activity = { module = "androidx.activity:activity", version.ref = "androidxActivity" } @@ -143,7 +145,7 @@ com-squareup-okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logg com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" } com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" } compose-animation-animationgraphics = { group = "androidx.compose.animation", name = "animation-graphics" } -compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } +compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "androidxComposeBom" } compose-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation" } compose-foundation-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } compose-material-iconscore = { group = "androidx.compose.material", name = "material-icons-core" } diff --git a/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics.png b/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics.png index 0868e0dbdb..6138797b8d 100644 --- a/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics.png +++ b/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae78b0fe1000514d126748fed3082181646642e5351f74ba5d7a7cc002c2d6b8 -size 28636 +oid sha256:11402e3914bfcd003f229ef3703db0bf19ba49f41741c103f9c45f35610dbb52 +size 28613 diff --git a/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics_rtl.png b/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics_rtl.png index 1cc6aa1759..6ec737dd45 100644 --- a/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics_rtl.png +++ b/health/composables/src/test/snapshots/images/com.google.android.horologist.health.composables.screens_MetricsScreenA11yTest_metricsScreenTwoMetrics_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc679357f057b70b81206284f7aa7ae96e82eccf6516db92dc12bdbe823c7ad7 -size 28532 +oid sha256:393a64ab93d67d43100770bfccbeffdf09d243b0a201a00a29642e6856fab4f0 +size 28517 diff --git a/media/ui/src/test/java/com/google/android/horologist/media/ui/MediaPlayerA11yScreenshotTest.kt b/media/ui/src/test/java/com/google/android/horologist/media/ui/MediaPlayerA11yScreenshotTest.kt index 82f2b20d54..5ecbd61a81 100644 --- a/media/ui/src/test/java/com/google/android/horologist/media/ui/MediaPlayerA11yScreenshotTest.kt +++ b/media/ui/src/test/java/com/google/android/horologist/media/ui/MediaPlayerA11yScreenshotTest.kt @@ -50,6 +50,9 @@ class MediaPlayerA11yScreenshotTest : WearLegacyA11yTest() { mediaPlayerScreen() } + override val runAtf: Boolean + get() = false + private fun mediaPlayerScreen() { val playerUiState = PlayerUiState( playEnabled = true, diff --git a/media/ui/src/test/snapshots/images/com.google.android.horologist.media.ui_MediaPlayerA11yScreenshotTest_mediaPlayerLargeRound.png b/media/ui/src/test/snapshots/images/com.google.android.horologist.media.ui_MediaPlayerA11yScreenshotTest_mediaPlayerLargeRound.png index b6381cf3ee..308a070033 100644 --- a/media/ui/src/test/snapshots/images/com.google.android.horologist.media.ui_MediaPlayerA11yScreenshotTest_mediaPlayerLargeRound.png +++ b/media/ui/src/test/snapshots/images/com.google.android.horologist.media.ui_MediaPlayerA11yScreenshotTest_mediaPlayerLargeRound.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c2071e1143891451d9d3abf7b59f17b4471d2def4a7c096e05052caad84da9 -size 118004 +oid sha256:0ae9899f7af174e67ba715657e6d0199e2152100cfc5709894593a8a0cf65af7 +size 117995 diff --git a/roboscreenshots/api/current.api b/roboscreenshots/api/current.api index 9fd6e867d4..78242428ee 100644 --- a/roboscreenshots/api/current.api +++ b/roboscreenshots/api/current.api @@ -74,14 +74,16 @@ package com.google.android.horologist.screenshots.rng { method @org.robolectric.ParameterizedRobolectricTestRunner.Parameters public java.util.List devices(); } - @org.junit.runner.RunWith(AndroidJUnit4::class) @org.robolectric.annotation.Config(sdk={33}, qualifiers=com.github.takahirom.roborazzi.RobolectricDeviceQualifiers.WearOSLargeRound) @org.robolectric.annotation.GraphicsMode(org.robolectric.annotation.GraphicsMode.Mode.NATIVE) public abstract class WearLegacyA11yTest { + @org.junit.runner.RunWith(AndroidJUnit4::class) @org.robolectric.annotation.Config(sdk={34}, qualifiers=com.github.takahirom.roborazzi.RobolectricDeviceQualifiers.WearOSLargeRound) @org.robolectric.annotation.GraphicsMode(org.robolectric.annotation.GraphicsMode.Mode.NATIVE) public abstract class WearLegacyA11yTest { ctor public WearLegacyA11yTest(); method @androidx.compose.runtime.Composable public void ComponentScaffold(kotlin.jvm.functions.Function0 content); method @androidx.compose.runtime.Composable public void TestScaffold(kotlin.jvm.functions.Function0 content); method public final void captureScreenA11yRoboImage(String filePath, com.github.takahirom.roborazzi.RoborazziOptions roborazziOptions); method public final void captureScreenshot(optional String suffix); + method public void configureAccessibilityValidator(com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator validator); method @org.junit.Rule public final androidx.compose.ui.test.junit4.ComposeContentTestRule getComposeRule(); method public coil.test.FakeImageLoaderEngine? getImageLoader(); + method public boolean getRunAtf(); method @org.junit.Rule public final org.junit.rules.TestName getTestInfo(); method public float getTolerance(); method public final void runComponentTest(optional androidx.compose.ui.graphics.Color? background, kotlin.jvm.functions.Function0 content); @@ -89,6 +91,7 @@ package com.google.android.horologist.screenshots.rng { method public String testName(String suffix); property @org.junit.Rule public final androidx.compose.ui.test.junit4.ComposeContentTestRule composeRule; property public coil.test.FakeImageLoaderEngine? imageLoader; + property public boolean runAtf; property @org.junit.Rule public final org.junit.rules.TestName testInfo; property public float tolerance; field public static final com.google.android.horologist.screenshots.rng.WearLegacyA11yTest.Companion Companion; diff --git a/roboscreenshots/build.gradle.kts b/roboscreenshots/build.gradle.kts index e8f0890235..3f708c6b7d 100644 --- a/roboscreenshots/build.gradle.kts +++ b/roboscreenshots/build.gradle.kts @@ -90,6 +90,7 @@ dependencies { api(projects.composeLayout) api(projects.images.coil) api(projects.tiles) + api(libs.accessibility.test.framework) api(libs.kotlin.stdlib) api(libs.okio) diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/ExtraShadowAccessibilityManager.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/ExtraShadowAccessibilityManager.kt new file mode 100644 index 0000000000..c17652e093 --- /dev/null +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/ExtraShadowAccessibilityManager.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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 com.google.android.horologist.screenshots.a11y + +import android.annotation.SuppressLint +import android.view.accessibility.AccessibilityManager +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.shadows.ShadowAccessibilityManager +import org.robolectric.versioning.AndroidVersions.U + +@Implements(AccessibilityManager::class) +internal class ExtraShadowAccessibilityManager : ShadowAccessibilityManager() { + + /** + * This shadow method is required because {@link + * android.view.accessibility.DirectAccessibilityConnection} calls it to determine if any + * transformations have occurred on this window. + */ + @SuppressLint("PrivateApi") + @Implementation(minSdk = U.SDK_INT) + fun getWindowTransformationSpec(windowId: Int): Any { + val instance = + Class.forName("android.view.accessibility.IAccessibilityManager\$WindowTransformationSpec") + .getDeclaredConstructor().newInstance() + + return instance + } +} diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/NonShadowShadowAccessibilityNodeInfo.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/NonShadowShadowAccessibilityNodeInfo.kt new file mode 100644 index 0000000000..a5d740ce67 --- /dev/null +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/a11y/NonShadowShadowAccessibilityNodeInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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 com.google.android.horologist.screenshots.a11y + +import android.view.accessibility.AccessibilityNodeInfo +import org.robolectric.annotation.Implements + +@Implements(AccessibilityNodeInfo::class) +internal class NonShadowShadowAccessibilityNodeInfo diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearLegacyA11yTest.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearLegacyA11yTest.kt index 9e58ebb098..9aab907fec 100644 --- a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearLegacyA11yTest.kt +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearLegacyA11yTest.kt @@ -20,6 +20,7 @@ package com.google.android.horologist.screenshots.rng import android.app.Application import android.graphics.Bitmap +import android.os.Build import android.os.Looper import android.view.accessibility.AccessibilityManager import androidx.compose.foundation.background @@ -29,9 +30,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso import androidx.test.espresso.Root @@ -46,6 +49,7 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.ThresholdValidator import com.github.takahirom.roborazzi.captureRoboImage +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator import com.google.android.horologist.compose.layout.AppScaffold import com.google.android.horologist.compose.layout.ResponsiveTimeText import com.google.android.horologist.screenshots.FixedTimeSource @@ -59,9 +63,10 @@ import org.junit.runner.RunWith import org.robolectric.Shadows import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode +import org.robolectric.shadows.ShadowBuild @Config( - sdk = [33], + sdk = [34], qualifiers = RobolectricDeviceQualifiers.WearOSLargeRound, ) @RunWith(AndroidJUnit4::class) @@ -78,6 +83,12 @@ public abstract class WearLegacyA11yTest { public open val imageLoader: FakeImageLoaderEngine? = null + public open val runAtf: Boolean + get() = true + + public open fun configureAccessibilityValidator(validator: AccessibilityValidator) { + } + public fun runScreenTest( content: @Composable () -> Unit, ) { @@ -87,6 +98,19 @@ public abstract class WearLegacyA11yTest { } } + if (runAtf && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // TODO change this check in ATF + ShadowBuild.setFingerprint("test_fingerprint") + + composeRule.enableAccessibilityChecks() + + val accessibilityValidator = + (composeRule as AndroidComposeTestRule<*, *>).accessibilityValidator!! + configureAccessibilityValidator(accessibilityValidator) + + composeRule.onRoot().tryPerformAccessibilityChecks() + } + captureScreenshot() }