Skip to content

Commit

Permalink
Adds ATF Accessibility checks to roboscreenshots (#2448)
Browse files Browse the repository at this point in the history
This commit adds accessibility tests for the Box composable. It also adds a shadow for AccessibilityManager and a non-shadow shadow for AccessibilityNodeInfo. The dependencies are updated to include the Accessibility Test Framework.

Adds Accessibility testing utilities to roboscreenshots to allow for accessibility tests in Robolectric screenshots.
  • Loading branch information
yschimke authored Nov 7, 2024
1 parent 78eed9e commit 38a63d2
Show file tree
Hide file tree
Showing 14 changed files with 117 additions and 16 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions datalayer/phone-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion datalayer/watch/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ package com.google.android.horologist.datalayer.watch {
method public suspend Object? markSetupNoLongerComplete(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public suspend Object? markTileAsInstalled(String tileName, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public suspend Object? markTileAsRemoved(String tileName, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @CheckResult public suspend Object? startCompanion(String nodeId, kotlin.coroutines.Continuation<? super error.NonExistentClass>);
method @CheckResult public suspend Object? startCompanion(String nodeId, kotlin.coroutines.Continuation<? super com.google.android.horologist.data.AppHelperResultCode>);
method public suspend Object? updateInstalledTiles(kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public kotlinx.coroutines.flow.Flow<java.util.Set<com.google.android.gms.wearable.Node>> connectedAndInstalledNodes;
property public final kotlinx.coroutines.flow.Flow<com.google.android.horologist.data.SurfacesInfo> surfacesInfo;
Expand Down
8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class MediaPlayerA11yScreenshotTest : WearLegacyA11yTest() {
mediaPlayerScreen()
}

override val runAtf: Boolean
get() = false

private fun mediaPlayerScreen() {
val playerUiState = PlayerUiState(
playEnabled = true,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion roboscreenshots/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,24 @@ package com.google.android.horologist.screenshots.rng {
method @org.robolectric.ParameterizedRobolectricTestRunner.Parameters public java.util.List<com.google.android.horologist.screenshots.rng.WearDevice> 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<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void TestScaffold(kotlin.jvm.functions.Function0<kotlin.Unit> 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<kotlin.Unit> content);
method public final void runScreenTest(kotlin.jvm.functions.Function0<kotlin.Unit> content);
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;
Expand Down
1 change: 1 addition & 0 deletions roboscreenshots/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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,
) {
Expand All @@ -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()
}

Expand Down

0 comments on commit 38a63d2

Please sign in to comment.