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

REPLAY-1330 Support UIDatePicker elements in SR (date & time selection) #1204

Merged
merged 4 commits into from
Mar 21, 2023

Conversation

ncreated
Copy link
Member

@ncreated ncreated commented Mar 10, 2023

What and why?

📦 This PR enhances the recording of UIDatePicker elements in session replay - in all modes (date, time, date & time) and all styles (compact, inline, wheels). Tested on iOS 13.7, 14.5, 15.5. Snapshots generated for 16.2:

Date selection:

With no masking & light mode:With masking & dark mode:

Time selection:

With no masking & light mode:With masking & dark mode:

How?

At the highest level, UIDatePickerRecorder uses one of 3 private recorders depending on its datePickerStyle:

internal struct UIDatePickerRecorder: NodeRecorder {
    private let wheelsStyleRecorder = WheelsStyleDatePickerRecorder()
    private let compactStyleRecorder = CompactStyleDatePickerRecorder()
    private let inlineStyleRecorder = InlineStyleDatePickerRecorder()

    // ...
}

The WheelsStyleDatePickerRecorder uses UIPickerViewRecorder (which is how UIKit handles the .wheels mode). I was able to reuse 90% of work done for picker views - but the strategy of choosing "selected labels" turned out to be flaky. I enhanced it by much better predicate that depends solely on transform3D and labels visibility:

if attributes.isTranslucent || !CATransform3DIsIdentity(view.transform3D) {
   // If this view has any 3D effect applied, ignore it and do not enter its subtree:
   return IgnoredElement(subtreeStrategy: .ignore)
}

Other than this, I had to improve text obfuscation logic. While we want to obfuscate labels in UIPickerViewRecorder, according to product definition we don't want this if that recorder is ran for WheelsStyleDatePickerRecorder subtree. What I propose in this PR is having 3 separate text obfuscators in the context propagated during view-tree traversal. Parent recorders can now adjust selectionTextObfuscator accordingly before passing it to child recorders 👌:

internal struct ViewTreeRecordingContext {
    /// ...

    /// Text obfuscator applied to all non-sensitive texts. No-op if privacy mode is disabled.
    var textObfuscator: TextObfuscating
    /// Text obfuscator applied to user selection texts (such as labels in picker control).
    var selectionTextObfuscator: TextObfuscating
    /// Text obfuscator applied to all sensitive texts (such as passwords or e-mail address).
    let sensitiveTextObfuscator: TextObfuscating
}

Last, I did few changes to SRSnapshotTests project so it can be ran on iOS 13.4 and above. Snapshot testing is still limited to iOS 16.2, but it is possible to generate images manually for older ones.

Review checklist

  • Feature or bugfix MUST have appropriate tests (unit, integration)
  • Make sure each commit and the PR mention the Issue number or JIRA reference
  • Add CHANGELOG entry for user facing changes

Custom CI job configuration (optional)

  • Run unit tests
  • Run integration tests
  • Run smoke tests

@ncreated ncreated self-assigned this Mar 10, 2023
@ncreated ncreated marked this pull request as ready for review March 10, 2023 14:59
@ncreated ncreated requested a review from a team as a code owner March 10, 2023 14:59
Copy link
Member

@maciejburda maciejburda left a comment

Choose a reason for hiding this comment

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

Amazing work really!

It's easy to follow the solution and learn about the edge cases.

Left some minor comments 🙌

@@ -439,6 +443,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
Copy link
Member

Choose a reason for hiding this comment

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

Great! Is there a hard constraint on having 13.0 here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought there is some, but turned out there is not 🙂. Changed to 13.0 👍

@@ -63,6 +63,14 @@ internal class SnapshotTestCase: XCTestCase {
return createSideBySideImage(appImage, wireframesImage)
}

func wait(seconds: TimeInterval) {
Copy link
Member

Choose a reason for hiding this comment

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

maybe could be a part of TestUtilities ?

Copy link
Member Author

Choose a reason for hiding this comment

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

If only we will need it anywhere else, we can move it. I would add things to TestUtilities only when we see opportunity - otherwise it would become bloated.

nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context)
}

let isDisplayedInPopover: Bool = {
Copy link
Member

Choose a reason for hiding this comment

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

Probably a style preference on my side, but it's a perfect candidate for nested function. IMO slightly more swiftly. Obviously not a blocker :)

borderColor: isDisplayedInPopover ? SystemColors.secondarySystemFill : nil,
borderWidth: isDisplayedInPopover ? 1 : 0,
backgroundColor: isDisplayedInPopover ? SystemColors.secondarySystemGroupedBackground : SystemColors.systemBackground,
cornerRadius: 10,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe worth getting that from the layer? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

We can't - if date picker is shown in a popover, the background comes from parent's _UIVisualEffectBackdropView child, not the pickerView.layer itself. Doing arbitrary parent tree traversal would be flaky and only work on certain OSes (where popover is displayed). To solve this complexity, we use 10 as approximation.

internal struct UIImageViewRecorder: NodeRecorder {
internal class UIImageViewRecorder: NodeRecorder {
/// An option for overriding default semantics from parent recorder.
var semanticsOverride: (UIImageView, ViewAttributes) -> NodeSemantics? = { _, _ in nil }
Copy link
Member

Choose a reason for hiding this comment

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

nice!

Comment on lines 72 to 76
let className = "\(type(of: imageView))"
// This gets effective on iOS 15.0+ which is the earliest version that displays some elements in popover views.
// Here we explicitly ignore the "shadow" effect applied to popover.
let isSystemShadow = className == "_UICutoutShadowView"
return isSystemShadow ? IgnoredElement(subtreeStrategy: .ignore) : nil
Copy link
Member

Choose a reason for hiding this comment

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

It almost sounds like it should be default value of the semanticsOverride in the init.

We probably want to expand the coverage of non-essential images here as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds fair 👍, moved this to default value on UIImageViewRecorder.

@ncreated ncreated merged commit 4911a71 into develop Mar 21, 2023
@ncreated ncreated deleted the ncreated/REPLAY-1330-support-date-pickers branch March 21, 2023 14:17
@ncreated ncreated mentioned this pull request Mar 31, 2023
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants