-
Notifications
You must be signed in to change notification settings - Fork 73
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
Changes from all commits
17c9ffe
6b76df3
6d8e698
f9467d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -38,6 +39,7 @@ extension FBSnapshotTestCase { | |
public func SnapshotVerifyAccessibility( | ||
_ view: UIView, | ||
identifier: String = "", | ||
forceCursorVisible: Bool = true, | ||
showActivationPoints activationPointDisplayMode: ActivationPointDisplayMode = .whenOverridden, | ||
useMonochromeSnapshot: Bool = true, | ||
markerColors: [UIColor] = [], | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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), | ||
|
@@ -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. | ||
|
@@ -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 | ||
} | ||
|
||
} |
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) | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.