Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to force the cursor to stay visible in snapshots for all text fields #17

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extension FBSnapshotTestCase {
/// - parameter view: The view that will be snapshotted.
/// - parameter identifier: An optional identifier included in the snapshot name, for use when there are multiple
/// snapshot tests in a given test method. Defaults to no identifier.
/// - parameter forceCursorVisible: Whether or not the text fields in the view (if any) should be showing their cursor in the snapshot. Defaults to `true`.
/// - parameter showActivationPoints: When to show indicators for elements' accessibility activation points.
/// Defaults to showing activation points only when they are different than the default activation point for that
/// element.
Expand All @@ -38,6 +39,7 @@ extension FBSnapshotTestCase {
public func SnapshotVerifyAccessibility(
_ view: UIView,
identifier: String = "",
forceCursorVisible: Bool = true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to say this is a patch change since it addresses flaky tests, but I think defaulting this to true makes it a breaking change, since tests can start failing if consumers already had a different fix in place - for example, pausing the animation in a similar way except setting the offset to a point where the cursor is hidden.

To be clear, making a breaking change is totally fine, just wanted to call it out here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, probably a breaking change.

showActivationPoints activationPointDisplayMode: ActivationPointDisplayMode = .whenOverridden,
useMonochromeSnapshot: Bool = true,
markerColors: [UIColor] = [],
Expand All @@ -53,6 +55,25 @@ extension FBSnapshotTestCase {
return
}

// Store the layers of the text fields whose animations will be
// paused in order to resume them after the snapshot test.
var pausedTextFieldLayers = [CALayer]()
if forceCursorVisible {
// Show the cursor of all text fields.
view.recursiveForEach(viewType: UITextField.self) { textField in
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realizing this isn't quite the behavior we want actually. Going to iterate on this a bit more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I'm not sure exactly what this will do right now. I think setting it to the converted CACurrentMediaTime() is going to pause the animation in its current position. Instead we probably want something like:

func pauseAnimation() -> (_ speed: Float, _ timeOffset: CFTimeInterval) {
    let originalValues = (speed, timeOffset)
    speed = 0
    timeOffset = 0
    return originalValues
}

func resumeAnimation(originalSpeed: Float, originalTimeOffset: CFTimeInterval) {
    timeOffset = originalTimeOffset
    speed = originalSpeed
}

textField.becomeFirstResponder()

// Find the field editor of the text field which controls the animation of the cursor
guard let fieldEditorLayer = (textField.value(forKey: "fieldEditor") as? UIView)?.layer else {
return
}

// Pause the animation of the layer so that the cursor will remain visible
fieldEditorLayer.pauseAnimation()
pausedTextFieldLayers.append(fieldEditorLayer)
}
}

let containerView = AccessibilitySnapshotView(
containedView: view,
viewRenderingMode: (usesDrawViewHierarchyInRect ? .drawHierarchyInRect : .renderLayerInContext),
Expand All @@ -69,6 +90,8 @@ extension FBSnapshotTestCase {
containerView.sizeToFit()

FBSnapshotVerifyView(containerView, identifier: identifier, file: file, line: line)

pausedTextFieldLayers.forEach { $0.resumeAnimation() }
}

/// Snapshots the `view` using the specified content size category to test Dynamic Type.
Expand Down Expand Up @@ -189,3 +212,39 @@ extension FBSnapshotTestCase {
}

}

// MARK: -

private extension UIView {

func recursiveForEach<ViewType: UIView>(
viewType: ViewType.Type,
_ block: (ViewType) -> Void
) {
if let view = self as? ViewType {
block(view)
}
subviews.forEach { $0.recursiveForEach(viewType: viewType, block) }
}

}

// MARK: -

private extension CALayer {

func pauseAnimation() {
speed = 0.0
timeOffset = convertTime(CACurrentMediaTime(), from: nil)
}

func resumeAnimation() {
let pausedTime = timeOffset
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime
beginTime = timeSincePause
}

}
8 changes: 8 additions & 0 deletions Example/AccessibilitySnapshot.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
66E2CD14CD63946657E17B15 /* Pods_SnapshotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A3192D7B9B16BD10FB517A2 /* Pods_SnapshotTests.framework */; };
C47F6C5316FB0C043BEB59F3 /* Pods_AccessibilitySnapshotDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A886964D2E787399E137105 /* Pods_AccessibilitySnapshotDemo.framework */; };
D38F6F4E508A3D067D677F69 /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BFCB4FD6BC17AB232B26E72 /* Pods_UnitTests.framework */; };
D70C820324DA07DE00550C3B /* TextFieldCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C820224DA07DE00550C3B /* TextFieldCursorTests.swift */; };
D70C820524DA097A00550C3B /* ContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C820424DA097A00550C3B /* ContainerView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -129,6 +131,8 @@
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>"; };
D70C820224DA07DE00550C3B /* TextFieldCursorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCursorTests.swift; sourceTree = "<group>"; };
D70C820424DA097A00550C3B /* ContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerView.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 @@ -274,6 +278,7 @@
3DBAC28622406EBB00EF4D0A /* AccessibilityContainersTests.swift */,
607FACEB1AFB9204008FA782 /* AccessibilityPropertiesTests.swift */,
3D39BFAF2239BC42009C3EF4 /* ActivationPointTests.swift */,
D70C820424DA097A00550C3B /* ContainerView.swift */,
3DEBF24F22101EE40065424F /* DefaultControlsTests.swift */,
3DF464FF220D5FB00048D446 /* ElementSelectionTests.swift */,
3DF46503220D8C500048D446 /* ElementOrderTests.swift */,
Expand All @@ -284,6 +289,7 @@
3D13DB512221124000066519 /* ObjectiveCTests.m */,
3DBEAA5C2223C0CE00FAE61D /* SwitchControlsTests.swift */,
3DC8D05A224750F500E8A03C /* TextAccessibilityTests.swift */,
D70C820224DA07DE00550C3B /* TextFieldCursorTests.swift */,
3DF464FB220D4F430048D446 /* SnapshotTestCase.swift */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
Expand Down Expand Up @@ -615,6 +621,8 @@
3DC488392212B40C006D1E15 /* ModalTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AccessibilityPropertiesTests.swift in Sources */,
3D39BFB02239BC42009C3EF4 /* ActivationPointTests.swift in Sources */,
D70C820324DA07DE00550C3B /* TextFieldCursorTests.swift in Sources */,
D70C820524DA097A00550C3B /* ContainerView.swift in Sources */,
3DC2C67921F4478A003184E4 /* LayoutTests.swift in Sources */,
3DF46504220D8C500048D446 /* ElementOrderTests.swift in Sources */,
3DEBF25022101EE40065424F /* DefaultControlsTests.swift in Sources */,
Expand Down
54 changes: 54 additions & 0 deletions Example/SnapshotTests/ContainerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// Copyright 2020 Square 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 UIKit

final class ContainerView: UIView {

// MARK: - Life Cycle

init(subview: UIView) {
self.subview = subview

super.init(frame: .zero)

backgroundColor = .white

addSubview(subview)
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Private Properties

private let subview: UIView

// MARK: - UIView

override func layoutSubviews() {
subview.frame.size = subview.sizeThatFits(bounds.insetBy(dx: 10, dy: 10).size)
subview.alignToSuperview(.center)
}

override func sizeThatFits(_ size: CGSize) -> CGSize {
let subviewSize = subview.sizeThatFits(size)
return CGSize(width: size.width, height: subviewSize.height + 20)
}

}
51 changes: 6 additions & 45 deletions Example/SnapshotTests/DefaultControlsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class DefaultControlsTests: SnapshotTestCase {
func testDatePicker() {
let datePicker = UIDatePicker()

let container = ContainerView(control: datePicker)
let container = ContainerView(subview: datePicker)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
Expand All @@ -40,7 +40,7 @@ final class DefaultControlsTests: SnapshotTestCase {
pageControl.pageIndicatorTintColor = .darkGray
pageControl.currentPageIndicatorTintColor = .black

let container = ContainerView(control: pageControl)
let container = ContainerView(subview: pageControl)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
Expand All @@ -55,7 +55,7 @@ final class DefaultControlsTests: SnapshotTestCase {
segmentedControl.insertSegment(withTitle: "Segment C", at: 2, animated: false)
segmentedControl.selectedSegmentIndex = 1

let container = ContainerView(control: segmentedControl)
let container = ContainerView(subview: segmentedControl)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
Expand All @@ -69,7 +69,7 @@ final class DefaultControlsTests: SnapshotTestCase {
slider.maximumValue = 100
slider.value = 75

let container = ContainerView(control: slider)
let container = ContainerView(subview: slider)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
Expand All @@ -83,7 +83,7 @@ final class DefaultControlsTests: SnapshotTestCase {
stepper.maximumValue = 1
stepper.value = 0

let container = ContainerView(control: stepper)
let container = ContainerView(subview: stepper)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
Expand All @@ -95,49 +95,10 @@ final class DefaultControlsTests: SnapshotTestCase {
stepper.maximumValue = 1
stepper.value = 0

let container = ContainerView(control: stepper)
let container = ContainerView(subview: stepper)
container.frame.size = container.sizeThatFits(UIScreen.main.bounds.size)

SnapshotVerifyAccessibility(container)
}

}

// MARK: -

private final class ContainerView: UIView {

// MARK: - Life Cycle

init(control: UIControl) {
self.control = control

super.init(frame: .zero)

backgroundColor = .white

addSubview(control)
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Private Properties

private let control: UIControl

// MARK: - UIView

override func layoutSubviews() {
control.frame.size = control.sizeThatFits(bounds.insetBy(dx: 10, dy: 10).size)
control.alignToSuperview(.center)
}

override func sizeThatFits(_ size: CGSize) -> CGSize {
let controlSize = control.sizeThatFits(size)
return CGSize(width: size.width, height: controlSize.height + 20)
}

}
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading