From 5a9da9885d0210bd813a328a2e1bcaf88bf7e1d6 Mon Sep 17 00:00:00 2001 From: Lukas Liebl Date: Mon, 11 Nov 2024 08:41:07 +0100 Subject: [PATCH 1/4] Implements per default addition of one accessibility snapshot per view --- Package.swift | 6 ++++-- .../TestingExtensions/SnapshotTestBase.swift | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 6df7d63..20c5f9e 100644 --- a/Package.swift +++ b/Package.swift @@ -17,14 +17,16 @@ let package = Package( ], dependencies: [ .package(name: "swift-snapshot-testing", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", .upToNextMajor(from: "1.12.0")), - .package(name: "SwiftRex", url: "https://github.com/SwiftRex/SwiftRex.git", .upToNextMajor(from: "0.8.12")) + .package(name: "SwiftRex", url: "https://github.com/SwiftRex/SwiftRex.git", .upToNextMajor(from: "0.8.12")), + .package(name: "AccessibilitySnapshot", url: "https://github.com/cashapp/AccessibilitySnapshot.git", from: "0.7.0"), ], targets: [ .target( name: "TestingExtensions", dependencies: [ .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "CombineRexDynamic", package: "SwiftRex") + .product(name: "CombineRexDynamic", package: "SwiftRex"), + .product(name: "AccessibilitySnapshot", package: "AccessibilitySnapshot") ] ) ] diff --git a/Sources/TestingExtensions/SnapshotTestBase.swift b/Sources/TestingExtensions/SnapshotTestBase.swift index 9a4c46e..5d7c8fb 100644 --- a/Sources/TestingExtensions/SnapshotTestBase.swift +++ b/Sources/TestingExtensions/SnapshotTestBase.swift @@ -11,6 +11,7 @@ import Foundation import SnapshotTesting import SwiftUI import XCTest +import AccessibilitySnapshot open class SnapshotTestBase: XCTestCase { public var allowAnimations: Bool = false @@ -28,10 +29,17 @@ open class SnapshotTestBase: XCTestCase { ("iPadPro", .iPadPro12_9(.portrait)) ] } + + open var accessibilityDevices: [(name: String, device: ViewImageConfig)] { + [ + ("iPhone13pro", .iPhone13) + ] + } open func assertSnapshotDevices( _ view: V, devices: [(name: String, device: ViewImageConfig)]? = nil, + accessibilityDevices: [(name: String, device: ViewImageConfig)]? = nil, style: [UIUserInterfaceStyle] = [.unspecified], imageDiffPrecision: Float = 1.0, file: StaticString = #file, @@ -64,6 +72,17 @@ open class SnapshotTestBase: XCTestCase { ) } } + + (accessibilityDevices ?? self.accessibilityDevices).forEach { config in + let vc = UIHostingController(rootView: view) + assertSnapshot( + of: vc, + as: .accessibilityImage(showActivationPoints: .always), + file: file, + testName: "\(testName)-\(config.name)-accessibility", + line: line + ) + } } } #endif From 44f3f0f5a95a7c3db731b24d9a39f7b94418b5b2 Mon Sep 17 00:00:00 2001 From: Lukas Liebl Date: Mon, 11 Nov 2024 08:41:23 +0100 Subject: [PATCH 2/4] Refactors suffix creation in snapshot assertion --- .../TestingExtensions/SnapshotTestBase.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/TestingExtensions/SnapshotTestBase.swift b/Sources/TestingExtensions/SnapshotTestBase.swift index 5d7c8fb..4630199 100644 --- a/Sources/TestingExtensions/SnapshotTestBase.swift +++ b/Sources/TestingExtensions/SnapshotTestBase.swift @@ -51,18 +51,19 @@ open class SnapshotTestBase: XCTestCase { let vc = UIHostingController(rootView: view) vc.overrideUserInterfaceStyle = uiStyle - let suffix: String - switch uiStyle { - case .unspecified: - suffix = "" - case .light: - suffix = "-light" - case .dark: - suffix = "-dark" - @unknown default: - fatalError("Unhandled UIUserInterfaceStyle \(uiStyle)") - } - + let suffix: String = { + switch uiStyle { + case .unspecified: + return "" + case .light: + return "-light" + case .dark: + return "-dark" + @unknown default: + fatalError("Unhandled UIUserInterfaceStyle \(uiStyle)") + } + }() + assertSnapshot( of: vc, as: .image(on: config.device, precision: imageDiffPrecision), From 196c32b1a125bf455c59c93144a10af1cba4637a Mon Sep 17 00:00:00 2001 From: Lukas Liebl Date: Wed, 27 Nov 2024 12:00:28 +0100 Subject: [PATCH 3/4] Refactored a11y snapshot hadling to be optionally enabled --- .../TestingExtensions/SnapshotTestBase.swift | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/Sources/TestingExtensions/SnapshotTestBase.swift b/Sources/TestingExtensions/SnapshotTestBase.swift index 4630199..d7cf72f 100644 --- a/Sources/TestingExtensions/SnapshotTestBase.swift +++ b/Sources/TestingExtensions/SnapshotTestBase.swift @@ -13,6 +13,19 @@ import SwiftUI import XCTest import AccessibilitySnapshot +extension SnapshotTestBase { + public typealias DeviceConfiguration = (name: String, device: ViewImageConfig) +} + +extension SnapshotTestBase { + /// Configuration of Accessibility snapshots. + public enum A11ySnapshotConfiguration { + case disabled + case enabled + case enabledWithDevices([DeviceConfiguration]) + } +} + open class SnapshotTestBase: XCTestCase { public var allowAnimations: Bool = false @@ -21,7 +34,7 @@ open class SnapshotTestBase: XCTestCase { UIView.setAnimationsEnabled(allowAnimations) } - open var defaultDevices: [(name: String, device: ViewImageConfig)] { + open var defaultDevices: [DeviceConfiguration] { [ ("iPhone8", .iPhone8), ("iPhone13proMax", .iPhone13ProMax), @@ -30,16 +43,27 @@ open class SnapshotTestBase: XCTestCase { ] } - open var accessibilityDevices: [(name: String, device: ViewImageConfig)] { + open var a11yDefaultDevices: [DeviceConfiguration] { [ ("iPhone13pro", .iPhone13) ] } - + + /// Asserts snapshots on the given devices (or default) respecting it's parameters. + /// - Parameters: + /// - view: The view to be snapshotted + /// - devices: Specified devices, if not given it'll take default devices + /// - a11ySnapshotConfiguration: Accessibility snapshot configuration, if enabled it adds a accessibility snapshot + /// - style: `UIUserInterfaceStyle` to be applied + /// - imageDiffPrecision: Precision of the compared images, 1 means it matches 100%, range is between 0 and 1. + /// - file: The file this was executed from, it will be taken as the snapshot's file name. + /// - testName: The test name taken as a part of the snapshot's file name. + /// - line: The line number on which failure occurred. Defaults to the line number on which this + /// function was called. open func assertSnapshotDevices( _ view: V, - devices: [(name: String, device: ViewImageConfig)]? = nil, - accessibilityDevices: [(name: String, device: ViewImageConfig)]? = nil, + devices: [DeviceConfiguration]? = nil, + a11ySnapshotConfiguration: A11ySnapshotConfiguration = .disabled, style: [UIUserInterfaceStyle] = [.unspecified], imageDiffPrecision: Float = 1.0, file: StaticString = #file, @@ -74,7 +98,19 @@ open class SnapshotTestBase: XCTestCase { } } - (accessibilityDevices ?? self.accessibilityDevices).forEach { config in + let a11ySnapshotDevices: [DeviceConfiguration]? = { + switch a11ySnapshotConfiguration { + case .disabled: + return nil + case .enabled: + return a11yDefaultDevices + case .enabledWithDevices(let specifiedDevices): + return specifiedDevices + } + }() + + guard let a11ySnapshotDevices else { return } + a11ySnapshotDevices.forEach { config in let vc = UIHostingController(rootView: view) assertSnapshot( of: vc, From c6fdce9bbe520123b03882ad2cd2e5037f807daa Mon Sep 17 00:00:00 2001 From: Lukas Liebl Date: Fri, 29 Nov 2024 11:15:21 +0100 Subject: [PATCH 4/4] Adds check if a11y snapshots are run in hosting application Otheriwse it'd throw an error from `AccessibilitySnapshots` which can be hard to detect in the CI. --- Sources/TestingExtensions/SnapshotTestBase.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/TestingExtensions/SnapshotTestBase.swift b/Sources/TestingExtensions/SnapshotTestBase.swift index d7cf72f..7753d81 100644 --- a/Sources/TestingExtensions/SnapshotTestBase.swift +++ b/Sources/TestingExtensions/SnapshotTestBase.swift @@ -110,11 +110,15 @@ open class SnapshotTestBase: XCTestCase { }() guard let a11ySnapshotDevices else { return } + + guard UIApplication.shared != nil else { + XCTFail("Accessibility snapshots must be run from a hosting application!") + } + a11ySnapshotDevices.forEach { config in - let vc = UIHostingController(rootView: view) assertSnapshot( - of: vc, - as: .accessibilityImage(showActivationPoints: .always), + of: view, + as: .accessibilityImage(showActivationPoints: .always, drawHierarchyInKeyWindow: true), file: file, testName: "\(testName)-\(config.name)-accessibility", line: line