Skip to content

Commit

Permalink
Merge pull request #119 from cashapp/entin/hit-test-snapshot
Browse files Browse the repository at this point in the history
Add utilities for highlighting hit test regions
  • Loading branch information
NickEntin authored Jan 19, 2024
2 parents 0095f44 + 907e743 commit a79b01b
Show file tree
Hide file tree
Showing 24 changed files with 790 additions and 165 deletions.
4 changes: 4 additions & 0 deletions Example/AccessibilitySnapshot.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
83A295842AC22D9D00DFBE4F /* UserInputLabelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A295832AC22D9D00DFBE4F /* UserInputLabelsViewController.swift */; };
83A295862AC22EEE00DFBE4F /* UserInputLabelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A295852AC22EEE00DFBE4F /* UserInputLabelsTests.swift */; };
C47F6C5316FB0C043BEB59F3 /* Pods_AccessibilitySnapshotDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A886964D2E787399E137105 /* Pods_AccessibilitySnapshotDemo.framework */; };
D2F76EED2945C879000A453F /* HitTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F76EEC2945C879000A453F /* HitTargetTests.swift */; };
D38F6F4E508A3D067D677F69 /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BFCB4FD6BC17AB232B26E72 /* Pods_UnitTests.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -141,6 +142,7 @@
88C33CBF672C290CE1EE86AF /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
C78F90CE7A2A315AADF80144 /* Pods-SnapshotTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests.debug.xcconfig"; sourceTree = "<group>"; };
CCFF2A604706B71DC0CBD38B /* Pods-AccessibilitySnapshotDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AccessibilitySnapshotDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-AccessibilitySnapshotDemo/Pods-AccessibilitySnapshotDemo.release.xcconfig"; sourceTree = "<group>"; };
D2F76EEC2945C879000A453F /* HitTargetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTargetTests.swift; sourceTree = "<group>"; };
DBA3D7413A13111BA7DE4750 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = "<group>"; };
DCFAC4866DBB341F92D0A40D /* Pods-SnapshotTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests.release.xcconfig"; sourceTree = "<group>"; };
ED63B7AD78B189E8940B6C80 /* Pods-AccessibilitySnapshotDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AccessibilitySnapshotDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AccessibilitySnapshotDemo/Pods-AccessibilitySnapshotDemo.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -302,6 +304,7 @@
3D9334932A8B2E520078A142 /* ImpreciseObjectiveCTests.m */,
3DBAC28622406EBB00EF4D0A /* AccessibilityContainersTests.swift */,
607FACEB1AFB9204008FA782 /* AccessibilityPropertiesTests.swift */,
D2F76EEC2945C879000A453F /* HitTargetTests.swift */,
3D39BFAF2239BC42009C3EF4 /* ActivationPointTests.swift */,
3DEBF24F22101EE40065424F /* DefaultControlsTests.swift */,
3DF46503220D8C500048D446 /* ElementOrderTests.swift */,
Expand Down Expand Up @@ -651,6 +654,7 @@
3D39BFB02239BC42009C3EF4 /* ActivationPointTests.swift in Sources */,
3DC2C67921F4478A003184E4 /* LayoutTests.swift in Sources */,
83A295862AC22EEE00DFBE4F /* UserInputLabelsTests.swift in Sources */,
D2F76EED2945C879000A453F /* HitTargetTests.swift in Sources */,
3DF46504220D8C500048D446 /* ElementOrderTests.swift in Sources */,
3DEBF25022101EE40065424F /* DefaultControlsTests.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>HitTargetTests</key>
<dict>
<key>testPerformance()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>48.792810</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>EDA548A6-E453-4CBA-9366-15D413356833</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>0</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Apple M1 Max</string>
<key>cpuSpeedInMHz</key>
<integer>0</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>modelCode</key>
<string>MacBookPro18,2</string>
<key>physicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>arm64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone13,3</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
<Test
Identifier = "DefaultControlsTests/testDatePicker()">
</Test>
<Test
Identifier = "HitTargetTests/testPerformance()">
</Test>
<Test
Identifier = "TextAccessibilityTests">
</Test>
Expand Down
83 changes: 83 additions & 0 deletions Example/SnapshotTests/HitTargetTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright 2023 Block Inc.
//
// 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
//
// http://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.
//

import AccessibilitySnapshot
import FBSnapshotTestCase
import Paralayout

@testable import AccessibilitySnapshotDemo

final class HitTargetTests: SnapshotTestCase {

func testButtonHitTarget() {
let buttonTraitsViewController = ButtonAccessibilityTraitsViewController()
buttonTraitsViewController.view.frame = UIScreen.main.bounds
SnapshotVerifyWithHitTargets(buttonTraitsViewController.view)
}

@available(iOS 14, *)
func testTableHitTarget() throws {
try XCTSkipUnless(
ProcessInfo().operatingSystemVersion.majorVersion >= 14,
"This test only supports iOS 14 and later"
)

let viewController = TableViewController()
viewController.view.frame = UIScreen.main.bounds
SnapshotVerifyWithHitTargets(viewController.view)
}

func testPerformance() throws {
let buttonTraitsViewController = ButtonAccessibilityTraitsViewController()
buttonTraitsViewController.view.frame = UIScreen.main.bounds

measure {
do {
_ = try HitTargetSnapshotUtility.generateSnapshotImage(
for: buttonTraitsViewController.view,
useMonochromeSnapshot: true,
viewRenderingMode: .drawHierarchyInRect
)
} catch {
XCTFail("Utility should not fail to generate snapshot image")
}
}
}

}

// MARK: -

@available(iOS 14, *)
private final class TableViewController: UITableViewController {

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()

var config = cell.defaultContentConfiguration()
config.text = "Hello World"
cell.contentConfiguration = config

cell.accessoryView = UISwitch()

return cell
}

}
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions Example/SnapshotTests/SnapshotTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ final class SnapshotTestingTests: XCTestCase {
assertSnapshot(matching: viewController, as: .imageWithSmartInvert, named: nameForDevice())
}

func testHitTargets() {
let viewController = ButtonAccessibilityTraitsViewController()
viewController.view.frame = UIScreen.main.bounds
assertSnapshot(matching: viewController, as: .imageWithHitTargets(), named: nameForDevice())
}

// MARK: - Private Methods

private func nameForDevice(baseName: String? = nil) -> String {
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.
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
@@ -1,5 +1,5 @@
//
// Copyright 2019 Square Inc.
// Copyright 2023 Block Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,6 @@
// limitations under the License.
//

import CoreImage
import UIKit

public enum ActivationPointDisplayMode {
Expand Down Expand Up @@ -786,161 +785,6 @@ private extension AccessibilitySnapshotView {

// MARK: -

private extension UIView {

func renderToImage(
monochrome: Bool,
viewRenderingMode: AccessibilitySnapshotView.ViewRenderingMode
) throws -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)

var error: Error?

let snapshot = renderer.image { context in
switch viewRenderingMode {
case .drawHierarchyInRect:
if bounds.width > UIView.tileSideLength || bounds.height > UIView.tileSideLength {
drawTiledHierarchySnapshots(in: context, error: &error)
} else {
drawHierarchy(in: bounds, afterScreenUpdates: true)
}

case .renderLayerInContext:
layer.render(in: context.cgContext)
}
}

if let error = error {
throw error
}

if monochrome {
return try monochromeSnapshot(for: snapshot) ?? snapshot

} else {
return snapshot
}
}

private func monochromeSnapshot(for snapshot: UIImage) throws -> UIImage? {
if ProcessInfo().operatingSystemVersion.majorVersion == 13 {
// On iOS 13, the image filter silently fails for large images, "successfully" producing a blank output
// image. From testing, the maximum support size is 1365x1365 pt. Exceeding that in either dimension will
// result in a blank image.
let maximumSize = CGSize(width: 1365, height: 1365)
if snapshot.size.width > maximumSize.width || snapshot.size.height > maximumSize.height {
throw AccessibilitySnapshotView.Error.containedViewExceedsMaximumSize(
viewSize: snapshot.size,
maximumSize: maximumSize
)
}
}

guard let inputImage = CIImage(image: snapshot) else {
return nil
}

let monochromeFilter = CIFilter(
name: "CIColorControls",
parameters: [
kCIInputImageKey: inputImage,
kCIInputSaturationKey: 0,
]
)!

let context = CIContext()

guard
let outputImage = monochromeFilter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent)
else {
return nil
}

return UIImage(cgImage: cgImage)
}

private func drawTiledHierarchySnapshots(in context: UIGraphicsImageRendererContext, error: inout Error?) {
guard CATransform3DIsIdentity(layer.transform) else {
error = AccessibilitySnapshotView.Error.containedViewHasUnsupportedTransform(transform: layer.transform)
return
}

let originalSafeArea = bounds.inset(by: safeAreaInsets)

let originalSuperview = superview
let originalOrigin = frame.origin
let originalAutoresizingMask = autoresizingMask
defer {
originalSuperview?.addSubview(self)
frame.origin = originalOrigin
autoresizingMask = originalAutoresizingMask
}

let frameView = UIView(frame: frame)
originalSuperview?.addSubview(frameView)
defer {
frameView.removeFromSuperview()
}

autoresizingMask = []
frame.origin = .zero

let containerViewController = UIViewController()
let containerView = containerViewController.view!
containerView.frame = frame
containerView.autoresizingMask = []
containerView.addSubview(self)
frameView.addSubview(containerView)

// Run the run loop for one cycle so that the safe area changes caused by restructuring the view hierarhcy are
// propogated. Then calculate the required additional safe area insets to create the equivalent original safe
// area. This new change will be propogated automatically when we draw the hierarchy for the first time.
RunLoop.current.run(until: Date())
let currentSafeArea = containerView.convert(bounds.inset(by: safeAreaInsets), from: self)
containerViewController.additionalSafeAreaInsets = UIEdgeInsets(
top: originalSafeArea.minY - currentSafeArea.minY,
left: originalSafeArea.minX - currentSafeArea.minX,
bottom: currentSafeArea.maxY - originalSafeArea.maxY,
right: currentSafeArea.maxX - originalSafeArea.maxX
)

let bounds = self.bounds
var tileRect: CGRect = .zero

while tileRect.minY < bounds.maxY {
tileRect.origin.x = bounds.minX
tileRect.size.height = min(tileRect.minY + UIView.tileSideLength, bounds.maxY) - tileRect.minY

while tileRect.minX < bounds.maxX {
tileRect.size.width = min(tileRect.minX + UIView.tileSideLength, bounds.maxX) - tileRect.minX
frameView.frame.size = tileRect.size

// Move the origin of the `frameView` and `containerView` such that the frame is over the right area of
// the snapshotted view, but the snapshotted view stays fixed relative to the `frameView`'s superview
// (so the view's position on screen doesn't change).
frameView.frame.origin = CGPoint(x: tileRect.minX, y: tileRect.minY)
containerView.frame.origin = CGPoint(x: -tileRect.minX, y: -tileRect.minY)

UIGraphicsImageRenderer(bounds: frameView.bounds)
.image { _ in
frameView.drawHierarchy(in: frameView.bounds, afterScreenUpdates: true)
}
.draw(at: tileRect.origin)

tileRect.origin.x += UIView.tileSideLength
}

tileRect.origin.y += UIView.tileSideLength
}
}

private static let tileSideLength: CGFloat = 2000

}

// MARK: -

private extension Bundle {

private final class Sentinel {}
Expand Down
Loading

0 comments on commit a79b01b

Please sign in to comment.