Techbase Academy
Android · Lesson 48

Testing and Troubleshooting Accessibility

13 phasesAccessibility Test PackAndroid Studio
Phase 1 of 13
Lesson Introduction

You can follow every rule in lesson 47 and still ship a barrier. A description can be present but meaningless, a reading order can be technically valid but confusing, and a contrast ratio can pass while the screen is still tiring to read. Accessibility has to be tested, with a mix of manual checks by a human and automated checks that catch the mechanical mistakes every time you build.

This lesson covers Test your app's accessibility, Testing for Accessibility and the Make your Android app accessible quiz of the pathway (https://developer.android.com/courses/pathways/make-your-android-app-accessible), together with the accessibility-testing guidance at https://developer.android.com/guide/topics/ui/accessibility/testing.

By the end you will be able to:

  • Run a structured manual test with TalkBack, Switch Access, Voice Access, large text and grayscale.
  • Use Accessibility Scanner and read its suggestions.
  • Add automated accessibility checks to UI tests (for Views with Espresso, and for Compose).
  • Use lint and Studio inspections to catch issues early.
  • Write accessibility test cases with Compose test APIs: labels, roles, states, sizes.
  • Review your app from your users' perspective, and plan user testing with people with disabilities.
  • Build an accessibility checklist for your team, and answer the pathway's quiz questions.

Big idea: automated tools find roughly the mechanical problems (a missing label, a small target, low contrast). Humans find the problems that matter most: a confusing order, a useless description, a task that takes 40 swipes. You need both.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 13
Prerequisite Concepts
  • Accessibility fundamentals and common problems (lesson 46).
  • Compose accessibility features (lesson 47).
  • Compose UI testing (lesson 45) and Espresso basics (lesson 20 for test folders).
  • Android Studio tools (lesson 44).
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 13
Conceptual Understanding: A Layered Testing Strategy
text
   MANUAL (humans)                          AUTOMATED (machines)
 +-------------------------------+      +--------------------------------------+
 | 5. User testing with people   |      | 1. Lint + IDE inspections (build time)|
 |    who use assistive tech     |      | 2. Unit tests: contrast, strings      |
 | 4. Screen reader walkthrough  |      | 3. UI tests with accessibility checks |
 | 3. Large text / display size  |      | 4. Accessibility Scanner (semi-auto)  |
 +-------------------------------+      +--------------------------------------+
        slow, rich, subjective                fast, repeatable, shallow
Layer Finds Misses
Lint Missing contentDescription in XML, hard-coded text, small minHeight Anything dynamic
Unit tests Contrast of colour pairs, string quality Layout
UI tests with checks Missing labels, small targets, low contrast on real rendered UI Meaning, order, usability
Accessibility Scanner Similar to above on a running app, with screenshots Same limits
TalkBack walkthrough Poor order, bad descriptions, missing announcements, focus traps Not repeatable at scale
User testing Whether real people can complete real tasks Needs time and participants

A common figure quoted in the industry is that automated tools can catch only a minority of possible accessibility issues, so never treat a green automated check as proof of accessibility.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: A Layered Testing Strategy. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 13
Step-by-Step Build 1: A Manual TalkBack Test Script

A good manual test is a script, not a feeling. For each main task in your app (say, "add an item to the order and pay"), do this:

  1. Set up: a real device or an emulator image with Play Services and TalkBack (from the Android Accessibility Suite). Turn TalkBack on, and set speech rate to a speed you can follow. Start from the first screen with the screen dimmed or turned off if possible (some devices have a screen curtain option in TalkBack settings so you cannot rely on sight).
  2. Linear navigation: swipe right repeatedly from the top to the end of the screen. Listen to each stop. Ask: - Does every stop make sense on its own? ("Button" alone is a failure.) - Is the order logical? Does it match the visual order? - Is anything missing that you can see? Is anything announced that you cannot see? - Are there too many stops (every word in a card is its own stop)?
  3. Explore by touch: drag a finger over the screen. Does the element under your finger match what you hear?
  4. Activate each control with a double tap. Does the result get announced (focus moves sensibly, a message is spoken)?
  5. Forms: is each field announced with its label, hint and error? After a validation error, is the error announced and is focus placed on the problem field?
  6. Dialogs, menus, bottom sheets: does focus move into them, stay inside while open, and return to the trigger on close?
  7. Dynamic content: a toast, snackbar or loading finishing: does the user learn about it?
  8. Headings and landmarks: use TalkBack's headings navigation mode (or reading controls) to jump between headings. If it says "no headings", mark them.
  9. Lists: are item positions announced ("item 3 of 20")? Does a long list let you move quickly?
  10. Time: is any step timed with no way to extend?

Then repeat a lighter test with:

  • Switch Access (or a keyboard connected over USB or Bluetooth: Tab and arrow keys should move focus, Enter activates): can you reach and operate everything? Is the focus indicator visible?
  • Voice Access: say "Show labels" or "Show numbers", then try to tap controls by name. Controls whose visible text and accessible name differ ("Pay now" shown, "Submit" announced) are hard to say; keep them the same.
  • Largest font and display size: any clipping, overlap or hidden buttons? Landscape too.
  • Grayscale and high-contrast text: can you still tell states apart?
  • Remove animations: does everything still work?

Record every issue as a bug with: screen, step, expected, actual, assistive technology used and severity. Use the blocker/major/minor scale from lesson 46.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: A Manual TalkBack Test Script. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 13
Step-by-Step Build 2: Accessibility Scanner

Accessibility Scanner is a free app from Google (install it from the Play Store, then enable it in Settings → Accessibility). Once enabled, a floating check-mark button appears over every app.

  1. Open your app on the screen you want to check.
  2. Tap the floating button, then the tick. The scanner takes a snapshot.
  3. It highlights elements and lists suggestions, typically: - Touch target size: "Item's height is 32dp. Consider making the clickable area at least 48dp." - Text contrast: "Increase the contrast ratio to at least 4.5:1." - Item label: "This item may not have a label readable by screen readers." - Image contrast, Item type and others such as editable items and duplicated clickable bounds, depending on the version.
  4. Each suggestion has a Learn more link and the element's details.
  5. Fix, rebuild, rescan.

Limits: it checks what is on screen at the moment of the scan (so scan every state: empty, loading, error, filled), and it cannot judge whether a label is good. Save the scan results as part of the bug report.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Accessibility Scanner. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 13
Step-by-Step Build 3: Automated Checks in Compose Tests

Compose UI tests (lesson 45) can run accessibility checks over the rendered semantics and UI. The API for this has been evolving, and some functions carry an @ExperimentalTestApi annotation, so check the Testing your Compose layout and accessibility testing pages for the exact names in your Compose version. The general shape is:

kotlin
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class OrderScreenA11yTest {
 @get:Rule
 val rule = createComposeRule()
 @OptIn(ExperimentalTestApi::class)
 @Before
 fun enableChecks() {
 rule.enableAccessibilityChecks() // runs the Accessibility Test Framework on each action
 }
 @Test
 fun orderScreenHasNoCheckFailures() {
 rule.setContent { OrderScreen() }
 // Any action or assertion that triggers a check will fail the test with a clear message
 rule.onRoot().assertExists()
 }
}

When checks are enabled, the framework runs Google's Accessibility Test Framework (ATF) checks (touch target size, label presence, contrast of text where measurable, duplicated clickable bounds, and more) against the real rendered tree and fails the test if a check is violated. If an API in this form is not available in your version, you can still write your own explicit tests, as in the next sections.

Write your own explicit accessibility assertions

Explicit tests encode decisions your team made, and they work on any Compose version.

kotlin
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.unit.dp
import org.junit.Rule
import org.junit.Test
class OrderScreenSemanticsTest {
 @get:Rule
 val rule = createComposeRule()
 @Test
 fun deleteButtonIsLabelledAndBigEnough() {
 rule.setContent { OrderScreen() }
 rule.onNodeWithContentDescription("Delete Zobo")
 .assertHasClickAction()
 .assertWidthIsAtLeast(48.dp)
 .assertHeightIsAtLeast(48.dp)
 }
 @Test
 fun payIsAButton() {
 rule.setContent { OrderScreen() }
 rule.onNodeWithText("Pay now").assert(
 SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)
 )
 }
}

assertWidthIsAtLeast and assertHeightIsAtLeast measure the node's size in dp: this is how you check the 48 dp rule in a test. SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button) checks the role. Define small helper matchers when you repeat a check often:

kotlin
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
fun hasHeading() = SemanticsMatcher.keyIsDefined(SemanticsProperties.Heading)
fun hasLiveRegion() = SemanticsMatcher.keyIsDefined(SemanticsProperties.LiveRegion)
fun hasStateDescription(text: String) =
 SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, text)
kotlin
@Test
fun titleIsAHeading() {
 rule.setContent { OrderScreen() }
 rule.onNode(hasText("Your order") and hasHeading()).assertExists()
}

Test all the font sizes

kotlin
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.Density
import androidx.compose.material3.MaterialTheme
import org.junit.Test
@Test
fun payButtonStillVisibleAtLargestFont() {
 rule.setContent {
 val base = LocalDensity.current
 CompositionLocalProvider(LocalDensity provides Density(base.density, fontScale = 2f)) {
 MaterialTheme { OrderScreen() }
 }
 }
 rule.onNodeWithText("Pay now").assertIsDisplayed()
}

Unit-test your contrast

In lesson 47 you wrote contrastRatio. Put every custom colour pair of your design system into one test, so a designer's palette change that breaks accessibility fails the build:

ThemeContrastTest.kt
package com.example.accessdemo
import androidx.compose.ui.graphics.Color
import org.junit.Assert.assertTrue
import org.junit.Test
class ThemeContrastTest {
 private val pairs = mapOf(
 "primary on primaryContainer" to (Color(0xFF00210D) to Color(0xFFA8F5B7)),
 "onSurface on surface" to (Color(0xFF1A1C19) to Color(0xFFFCFDF7)),
 "onError on error" to (Color(0xFFFFFFFF) to Color(0xFFBA1A1A))
 )
 @Test
 fun allTextPairsMeetAA() {
 pairs.forEach { (name, colours) ->
 val ratio = contrastRatio(colours.first, colours.second)
 assertTrue("$name has contrast $ratio, needs 4.5", ratio >= 4.5f)
 }
 }
}
✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: Automated Checks in Compose Tests. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 13
Step-by-Step Build 4: Automated Checks for Views and Espresso

If your app still has View screens (lesson 42), Espresso can run the same ATF checks. Add the dependency androidx.test.espresso:espresso-accessibility as androidTestImplementation (use the version that matches your Espresso version) and enable checks once:

AccessibilityChecksSetup.kt
package com.example.classregister
import androidx.test.espresso.accessibility.AccessibilityChecks
import org.junit.BeforeClass
class AccessibleScreenTest {
 companion object {
 @JvmStatic
 @BeforeClass
 fun enableAccessibilityChecks() {
 AccessibilityChecks.enable().setRunChecksFromRootView(true)
 }
 }
 // Every ViewAction (click, typeText ...) now runs the checks on the views involved.
}

With setRunChecksFromRootView(true) the checks run against the whole screen, not just the view you touched. When a check fails, the exception message names the view, the check and the problem. You can suppress a known issue deliberately with setSuppressingResultMatcher, but record why, and fix it later.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Automated Checks for Views and Espresso. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 13
Step-by-Step Build 5: Lint and Inspections

Lint checks run on your source without launching the app:

  • Run: Analyze → Inspect Code in Studio, or in a terminal: ./gradlew lint (the HTML report is in app/build/reports/lint-results-debug.html).
  • For Views, lint flags ImageView and ImageButton without contentDescription (ContentDescription), touch targets smaller than 48 dp (TouchTargetSizeCheck), editable text without labels (LabelFor), colour contrast issues (TextContrastCheck), and hard-coded strings.
  • For Compose, lint has checks for common mistakes in modifiers and state, and some accessibility-related rules; the available checks vary by library version.
  • Treat accessibility warnings as errors in CI by adding a lint configuration:
build.gradle.kts
android {
 lint {
 warningsAsErrors = false
 error += setOf("ContentDescription", "LabelFor")
 abortOnError = true
 }
}

The error += setOf(...) line raises the severity of those two checks so the build fails if they appear. Be careful with warningsAsErrors = true: it can fail builds on unrelated warnings.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Lint and Inspections. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 13
Step-by-Step Build 6: Review from Your Users' Perspective

Tools check rules. People check reality. A practical review has four steps.

1. Write user stories with assistive technology

"As a blind student, I want to add my order and pay using TalkBack alone so that I do not need someone to help me." Write one per major task and per kind of need (screen reader, switch, voice, low vision, motor, hearing, cognitive).

2. Walk the stories

Do each task with only the assistive tech in question. Time it. If a sighted person needs 5 taps and a TalkBack user needs 40 swipes, the design needs shortcuts (headings, grouped items, fewer stops).

3. Check the content, not only the controls

  • Plain language. Short sentences, no jargon, one idea per instruction.
  • Errors that help: "Phone number must have 11 digits" is better than "Invalid input".
  • Consistency: the same icon and word always mean the same thing; navigation stays in the same place.
  • Forgiveness: confirmation before destructive actions; an undo.
  • No time pressure without a way to extend it.
  • No flashing more than three times per second.
  • Not only sound and not only colour.

4. Involve real users

Nothing replaces testing with people who use assistive technology daily. Reach out to local disability organisations, schools for the blind and deaf, or university disability services; offer fair payment or a clear benefit; explain what you will record; let them use their own device and settings; observe, do not coach; and treat every difficulty as a bug in the app, not a problem with the person. Always get consent and protect their privacy.

Even without participants, you can invite team members to use a "day without a screen" or one-handed constraints for an hour, as a start (but this builds empathy and does not replace real user research).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 6: Review from Your Users' Perspective. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 13
Step-by-Step Build 7: Troubleshooting Cookbook
Symptom Likely cause Fix
TalkBack says "Unlabelled" or just "Button" No contentDescription / text Add a purposeful description (lesson 47)
TalkBack reads text twice Description and visible text both on same control contentDescription = null on the icon
Row requires 4 swipes Children not merged mergeDescendants = true
A custom control not announced as button No role role = Role.Button or standard component
Switch announces nothing about state No toggleable toggleable(role = Role.Switch)
Error is never spoken Not a live region liveRegion = Polite
Swipe-to-dismiss unusable Gesture-only customActions
Text clipped at 200% Fixed height / maxLines heightIn(min); allow wrap
Tapping near an icon misses Touch area under 48 dp minimumInteractiveComponentSize()
Focus jumps to the wrong place after closing a dialog Focus not restored Return focus to the trigger; use the standard dialog
Order is wrong Overlays or floating elements isTraversalGroup, traversalIndex
Colours fail in dark mode only Hard-coded or unpaired colours Role pairs, contrast unit test
ATF check fails on a decorative element Decorative item clickable or described Remove click/description or use invisibleToUser()
Voice Access cannot select "Pay now" Spoken name differs from visible Make contentDescription match visible text
✏️ Your Task
Practise what you just learned about Step-by-Step Build 7: Troubleshooting Cookbook. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 13
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Write a TalkBack test script

Write a short test script (ten steps) for the task "register a student" in the Class Register app. Include at least two checks on forms and two on dynamic announcements.

A model script: (1) Turn on TalkBack, open the app, swipe to the title and confirm it is announced as a heading. (2) Swipe to "Student name": confirm the label, its type (edit box) and hint are announced. (3) Double-tap, type "A", and check the keyboard works and characters are echoed. (4) Swipe away: confirm the validation error "At least 2 letters" is announced without needing to find it. (5) Fix the name; confirm the Save button changes from disabled ("dimmed") to enabled. (6) Double-tap Save. (7) Confirm that a confirmation is announced (snackbar or live region). (8) Confirm that the new student appears in the list and that its row is read as one item with name and present state. (9) Use the actions menu on the row; confirm "Delete" exists. (10) Repeat the flow at 200% font, and with Switch Access or a keyboard. Record device, TalkBack version and any deviations.

🎯 Your Challenge

Exercise 2: Turn a bug into a test

A bug report says "the Pay button is too small for some users". Write a Compose test that fails now and passes when fixed, and describe the fix.

kotlin
@Test
fun payButtonMeetsMinimumTouchTarget() {
 rule.setContent { MaterialTheme { OrderScreen() } }
 rule.onNodeWithText("Pay now")
 .assertWidthIsAtLeast(48.dp)
 .assertHeightIsAtLeast(48.dp)
}

If the button was built as Box(Modifier.size(40.dp).clickable { }) the test fails. The fix is to use a Material Button (which already respects minimumInteractiveComponentSize) or to apply Modifier.minimumInteractiveComponentSize() or defaultMinSize(minHeight = 48.dp) to the custom control. The test stays in the suite to stop a regression.

🎯 Your Challenge

Exercise 3: Choose the method

For each issue say which method finds it best: automated check, Accessibility Scanner, manual TalkBack, or user testing. (a) Missing contentDescription on an icon. (b) A label that says "Image" on every product. (c) Contrast failure on hint text. (d) A flow that takes 40 swipes. (e) Whether a person with a tremor can use the swipe-to-delete safely.

(a) Automated check, lint, or Scanner (mechanical). (b) Manual TalkBack: the label exists but is useless, which tools cannot judge. (c) Scanner or an automated contrast unit test. (d) Manual TalkBack walkthrough (and later user testing). (e) User testing with real participants, since it depends on individual motor ability and the design of the gesture.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 13
Common Mistakes (and How to Fix Them)

Mistake #1: Relying only on Accessibility Scanner. Fix: add a TalkBack script and automated tests.

Mistake #2: Testing only one state. Fix: scan empty, loading, error and filled states.

Mistake #3: Suppressing checks without a note. Fix: record the reason and a ticket to fix it.

Mistake #4: Running TalkBack only in English with default speed. Fix: also test with your users' languages and a realistic speed.

Mistake #5: Letting designers change colours without contrast tests. Fix: a palette unit test.

Mistake #6: Testing with the developer's own phone settings only. Fix: test with large display size and font on a mid-range device.

Mistake #7: "Fixed" without re-testing in assistive tech. Fix: re-run the script after each fix.

Mistake #8: Recruiting no real users and calling it done. Fix: plan a session with real users before major releases.

Mistake #9: Treating accessibility bugs as low priority polish. Fix: triage by who is blocked.

Mistake #10: Not including accessibility in the definition of done. Fix: add the checklist to every feature.

✏️ Your Task
Practise what you just learned about Common Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 13 of 13
Knowledge Check

These five questions mirror the style of the pathway's final quiz.

Question 1. Which tool can scan a running app and suggest fixes for touch target size and contrast?

Accessibility Scanner.

Question 2. Why can automated checks not guarantee that an app is accessible?

They test mechanical rules (a label exists, a target is big enough) but cannot judge whether labels are meaningful, whether the reading order makes sense, or whether real people can complete tasks. Manual and user testing are required.

Question 3. How can a Compose test check the 48 dp touch target rule?

With assertWidthIsAtLeast(48.dp) and assertHeightIsAtLeast(48.dp) on the node, or with the built-in accessibility checks of the test rule.

Question 4. What should you do after finding an accessibility bug with TalkBack?

Record the screen, steps, expected and actual behaviour, the assistive technology and severity; fix it; add an automated test where possible; and re-run the manual script.

Question 5. Why test with real users who use assistive technology?

Because they reveal usability barriers that rules and tools cannot, such as slow or confusing flows, unhelpful wording and gestures that are hard to perform, and they use the technology with skills and settings a developer lacks.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Accessibility Test Pack

Project goal: Create a reusable accessibility test pack for the OrderScreen of lesson 47: a manual script, automated tests and a checklist you could hand to a team.

Requirements:

  1. A manual TalkBack script (a Markdown file) of at least ten steps for "change the quantity and pay".
  2. A Compose test class with at least six tests: label present and unique, 48 dp targets, role of Pay, heading present, error live region present when the limit is reached, and layout at font scale 2.0.
  3. A contrast test for every custom colour pair in your theme.
  4. Lint configured to treat ContentDescription as an error.
  5. A one-page checklist for your team's definition of done.
  6. An Accessibility Scanner run recorded in a short report: two findings you fixed.

Stretch goal: enable automated accessibility checks in your test rule (if your Compose version supports it), and make the build fail when an accessibility check fails.

OrderScreenA11yPackTest.kt
package com.example.accessdemo
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNode
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class OrderScreenA11yPackTest {
 @get:Rule
 val rule = createComposeRule()
 private fun hasHeading() = SemanticsMatcher.keyIsDefined(SemanticsProperties.Heading)
 private fun hasLiveRegion() = SemanticsMatcher.keyIsDefined(SemanticsProperties.LiveRegion)
 @Test
 fun deleteLabelsAreUnique() {
 rule.setContent { MaterialTheme { OrderScreen() } }
 rule.onNodeWithContentDescription("Delete Zobo").assertExists()
 rule.onNodeWithContentDescription("Delete Akara").assertExists()
 assertEquals(1, rule.onAllNodesWithContentDescription("Delete Zobo").fetchSemanticsNodes().size)
 }
 @Test
 fun deleteTargetsAre48dp() {
 rule.setContent { MaterialTheme { OrderScreen() } }
 rule.onNodeWithContentDescription("Delete Zobo")
 .assertHasClickAction()
 .assertWidthIsAtLeast(48.dp)
 .assertHeightIsAtLeast(48.dp)
 }
 @Test
 fun payIsAButton() {
 rule.setContent { MaterialTheme { OrderScreen() } }
 rule.onNodeWithText("Pay now").assert(
 SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)
 )
 }
 @Test
 fun titleIsAHeading() {
 rule.setContent { MaterialTheme { OrderScreen() } }
 rule.onNode(hasText("Your order") and hasHeading()).assertExists()
 }
 @Test
 fun limitErrorIsALiveRegion() {
 rule.setContent { MaterialTheme { OrderScreen() } } // Suya starts at the limit of 5
 rule.onNode(hasText("Error: maximum 5 of one item") and hasLiveRegion()).assertExists()
 }
 @Test
 fun payVisibleAtLargestFont() {
 rule.setContent {
 val base = LocalDensity.current
 CompositionLocalProvider(LocalDensity provides Density(base.density, fontScale = 2f)) {
 MaterialTheme { OrderScreen() }
 }
 }
 rule.onNodeWithText("Pay now").assertIsDisplayed()
 }
}

The manual script (A11Y_SCRIPT.md):

text
# TalkBack script: change quantity and pay
1. TalkBack on; open Order screen. Expect: "Your order, heading".
2. Swipe right: first row. Expect a single stop: "Zobo, 1 x 250 naira, 1 of 5 allowed".
3. Open TalkBack actions menu. Expect: Increase quantity, Decrease quantity.
4. Choose Increase quantity. Expect updated state announced.
5. Swipe to the delete button. Expect: "Delete Zobo, button".
6. Raise Suya to 5. Expect the error to be announced politely, without moving focus.
7. Swipe to Gift wrap. Expect "Gift wrap, switch, off"; double-tap; expect "on".
8. Swipe to Pay now. Expect "Pay now, button". Double-tap.
9. Repeat at font 200% and display size largest; nothing clipped.
10. Repeat with a Bluetooth keyboard (Tab/Enter): focus visible and every action reachable.

The lint block:

build.gradle.kts
android {
 lint {
 error += setOf("ContentDescription", "LabelFor", "TouchTargetSizeCheck")
 abortOnError = true
 }
}

The team definition of done (checklist):

  • Labels: every informative image/icon has a purposeful, unique description; decorative ones are null.
  • Targets: all interactive elements at least 48 dp.
  • Contrast: 4.5:1 text, 3:1 large text and icons, tested in light and dark.
  • Not by colour alone; no sound-only or flash-only information.
  • Roles, states and headings correct; groups merged sensibly.
  • Gestures have alternatives (visible control or custom actions).
  • Large text (200%) and display size verified; scrolls, no clipping.
  • TalkBack script run on every changed screen; Switch/keyboard pass for navigation changes.
  • Automated tests and lint green; Accessibility Scanner clean for new screens.
  • Real-user feedback gathered before major releases.
1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 48 project”.

3

On github.com click New repository, name it android-lesson-48-testing-and-troubleshooting-accessibility, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 48 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: A Layered Testing Strategy
  • Step-by-Step Build 1: A Manual TalkBack Test Script
  • Step-by-Step Build 2: Accessibility Scanner
  • Step-by-Step Build 3: Automated Checks in Compose Tests
  • Step-by-Step Build 4: Automated Checks for Views and Espresso
  • Step-by-Step Build 5: Lint and Inspections
  • Step-by-Step Build 6: Review from Your Users' Perspective
  • Step-by-Step Build 7: Troubleshooting Cookbook
  • Guided Practice Exercises
  • Common Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close