Setting Up Automated Accessibility Audits in Your iOS App CI/CD Pipeline
--
Ensuring the accessibility of your iOS app is paramount for an inclusive user experience. This blog post guides you through the process of seamlessly integrating automated accessibility audits into your CI/CD pipeline, allowing you to catch and address potential issues early in your development workflow.
Why Automated Accessibility Audits?
Automated accessibility audits help identify and rectify potential issues before your app reaches users. By incorporating these audits into your CI/CD pipeline, you can proactively address accessibility concerns throughout the development cycle.
Steps to Set Up Automated Accessibility Audits:
- Add a new accessibility audit test
testAccessibilityPokemonScreen()
in your XCTestCase file:
final class SampleAppForMediumUITests: XCTestCase {
private var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = true
app = XCUIApplication()
app.launch()
}
func testAccessibilityPokemonScreen() throws {
//Please note performAccessibilityAudit API is available on iOS 17.0 onwards only.
if #available(iOS 17.0, *) {
try app.performAccessibilityAudit()
}
}
}
Let’s first explore performAccessibilityAudit:
Let’s delve into performAccessibilityAudit
: Invoking this method within a test will comprehensively audit the accessibility of the current view in XCUIApplication, akin to the Accessibility Inspector tool. No explicit assertions are required; any identified issues will automatically trigger a failure in the XCTest.
By default, performAccessibilityAudit()
evaluates various accessibility types:
contrast
: Checks for sufficient contrast in elements (available on all platforms).elementDetection
: Ensures all elements are detectable and accessible (available on all platforms).hitRegion
: Verifies elements are hittable and not obscured by other views (available on all platforms).sufficientElementDescription
: Validates elements have appropriate descriptions and/or labels (available on all platforms).dynamicType
: Ensures text within elements scales with system font size changes (available on iOS, watchOS, tvOS but not on macOS).textClipped
: Confirms text is not truncated and remains visible (available on iOS, watchOS, tvOS but not on macOS).trait
: Checks for proper trait definitions (available on iOS, watchOS, tvOS but not on macOS).action
: (available to MacOS apps only).parentChild
: (available to MacOS apps only).
How about handling specific audit issues selectively?
If there are particular audit concerns you wish to overlook, you have the option to ignore them based on the element or audit type. Here’s an example where I choose to exclude dynamicType audit issues for a UITextField.
func testAccessibilityPokemonScreen() throws {
if #available(iOS 17.0, *) {
try app.performAccessibilityAudit() { issue in
var shouldIgnore = false
if let element = issue.element, element.identifier == "pikachu", issue.auditType == .dynamicType {
shouldIgnore = true
}
return shouldIgnore
}
}
The closure of the performAccessibilityAudit
method will be executed for every issue identified during the audit, affording you the flexibility to determine which issues to disregard within this context. In the aforementioned example, I am excluding the dynamicType audit issue for a UITextField with an accessibility identifier equal to 'pikachu.'
Additionally, you have the option to selectively run an accessibility audit for specific audit types. Consider the following example where I intend to execute an accessibility audit solely for contrast, dynamicType, and trait.
// Run accessibility audit for contrast, dynamicType, and trait only
try app.performAccessibilityAudit(for: [.contrast, .dynamicType, .trait])
Or everything except dynamicType
// Run accessibility audit for all audit types except dynamicType
try app.performAccessibilityAudit(for: .all.subtracting(.dynamicType))
Analyzing Test Failures:
Within Xcode under Test Reports, the error description will specify the type of audit issue identified and the associated element. In the provided screenshot of a test failure, it indicates that Dynamic Type is not supported for the UITextField with the accessibility identifier ‘pikachu.’ For more in-depth debugging, you can leverage Accessibility Inspector if necessary.
2. I recommend creating a method dedicated to testing accessibility within each screen class. Let’s proceed by creating a new screen class specifically for my ‘Pokemon List’ view.
import Foundation
import XCTest
class PokemonListScreen {
let app: XCUIApplication
init(app: XCUIApplication) {
self.app = app
}
func testAccessibility() throws {
if #available(iOS 17.0, *) {
try app.performAccessibilityAudit()
}
}
}
Now, in your XCUITest, invoke this method when the relevant view is being displayed in the app.
func testAccessibilityPokemonScreen() throws {
let pokemonListScreen = PokemonListScreen(app: app)
try pokemonListScreen.testAccessibility()
}
Following this pattern, create accessibility XCUITest for each screen.
If you’re unfamiliar with the concept of a screen class, I highly recommend checking out my blog on the Page Object Model pattern in XCTest: https://medium.com/next-level-swift/your-ultimate-xcuitest-ios-design-pattern-page-object-model-pom-bfb82b265bb0
3. Let’s create a dedicated Test Plan specifically for Accessibility Audit Tests. To do this, navigate to Edit Scheme > Test > Click “+”, and choose ‘Create empty test plan’. Within the newly created test plan, include all tests related to accessibility audits
4. All set! Now, effortlessly integrate the execution of this new Test Plan into your CI/CD process. If you’re employing xcodebuild to run your XCUITests on CI/CD runners, simply specify this test plan
xcodebuild test
-workspace SampleAppForMedium.xcworkspace
-scheme SampleAppForMedium
-sdk iphonesimulator
-destination platform="iOS Simulator",OS=17.0,name="iPhone 14"
-derivedDataPath ~/builds/DerivedData/SampleAppForMedium
-testPlan SampleApp_AccessibilityAudit_1.xctestplan
-parallel-testing-enabled YES
-parallel-testing-worker-count 2
-quiet
Sources: