Skip to content

Commit

Permalink
Merge pull request #1194 from DataDog/maciey/REPLAY-1448-uistepper
Browse files Browse the repository at this point in the history
REPLAY-1448 Add support for UIStepper
  • Loading branch information
maciejburda authored Mar 10, 2023
2 parents 6b2b8ad + 29da304 commit 9c78109
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal enum Fixture: CaseIterable {
case pickers
case switches
case textFields
case steppers

var menuItemTitle: String {
switch self {
Expand All @@ -31,6 +32,8 @@ internal enum Fixture: CaseIterable {
return "Switches"
case .textFields:
return "Text Fields"
case .steppers:
return "Steppers"
}
}

Expand All @@ -50,6 +53,8 @@ internal enum Fixture: CaseIterable {
return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Switches")
case .textFields:
return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "TextFields")
case .steppers:
return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Steppers")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,82 @@
</objects>
<point key="canvasLocation" x="2689" y="32"/>
</scene>
<!--View Controller-->
<scene sceneID="wvK-B5-W4A">
<objects>
<viewController storyboardIdentifier="Steppers" id="THO-zy-oY9" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Pdd-aH-q76">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="St0-h3-aHe">
<rect key="frame" x="8" y="67" width="377" height="265.33333333333331"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Right Enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SXX-XD-0Xa">
<rect key="frame" x="0.0" y="0.0" width="377" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" maximumValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="9sT-pk-4or">
<rect key="frame" x="0.0" y="28.333333333333329" width="377" height="32"/>
</stepper>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Left Enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Gnd-Cl-hQg">
<rect key="frame" x="0.0" y="68.333333333333343" width="377" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" maximumValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="ADD-Eh-qaV">
<rect key="frame" x="0.0" y="96.666666666666657" width="377" height="32"/>
</stepper>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Both Enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L3Q-TO-dtN">
<rect key="frame" x="0.0" y="136.66666666666666" width="377" height="20.333333333333343"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" maximumValue="11" translatesAutoresizingMaskIntoConstraints="NO" id="vfV-UE-h3A">
<rect key="frame" x="0.0" y="165" width="377" height="32"/>
</stepper>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Both Disabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ihl-RG-xzx">
<rect key="frame" x="0.0" y="205" width="377" height="20.333333333333343"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" maximumValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="Q0b-6w-boU">
<rect key="frame" x="0.0" y="233.33333333333331" width="377" height="32"/>
</stepper>
</subviews>
</stackView>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" maximumValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="6oQ-Gf-GXb">
<rect key="frame" x="149.66666666666666" y="396.33333333333331" width="94" height="32"/>
</stepper>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Outside of stack view:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="W12-r3-40u">
<rect key="frame" x="112.66666666666669" y="367" width="168" height="20.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="4fy-ha-2IE"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="6oQ-Gf-GXb" firstAttribute="top" secondItem="St0-h3-aHe" secondAttribute="bottom" constant="64" id="CTb-kF-AMF"/>
<constraint firstItem="4fy-ha-2IE" firstAttribute="trailing" secondItem="St0-h3-aHe" secondAttribute="trailing" constant="8" id="EnP-Vo-AFm"/>
<constraint firstItem="6oQ-Gf-GXb" firstAttribute="top" secondItem="W12-r3-40u" secondAttribute="bottom" constant="8.9999999999999432" id="Q0F-qn-HDk"/>
<constraint firstItem="St0-h3-aHe" firstAttribute="top" secondItem="4fy-ha-2IE" secondAttribute="top" constant="8" id="XUH-7Z-iW7"/>
<constraint firstItem="6oQ-Gf-GXb" firstAttribute="centerX" secondItem="4fy-ha-2IE" secondAttribute="centerX" id="cdY-87-IfA"/>
<constraint firstItem="St0-h3-aHe" firstAttribute="leading" secondItem="4fy-ha-2IE" secondAttribute="leading" constant="8" id="diy-hT-7Fa"/>
<constraint firstItem="W12-r3-40u" firstAttribute="centerX" secondItem="6oQ-Gf-GXb" secondAttribute="centerX" id="oed-07-Bh4"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kah-t2-Kq8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1063" y="675"/>
</scene>
</scenes>
<resources>
<image name="cloud.fill" catalog="system" width="128" height="87"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,22 @@ final class SRSnapshotTests: SnapshotTestCase {
record: recordingMode
)
}

func testSteppers() throws {
show(fixture: .steppers)

var image = try takeSnapshot(configuration: .init(privacy: .allowAll))
DDAssertSnapshotTest(
newImage: image,
snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-allowAll-privacy"),
record: recordingMode
)

image = try takeSnapshot(configuration: .init(privacy: .maskAll))
DDAssertSnapshotTest(
newImage: image,
snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-maskAll-privacy"),
record: recordingMode
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import UIKit

internal struct UIStepperRecorder: NodeRecorder {
func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? {
guard let stepper = view as? UIStepper else {
return nil
}
guard attributes.isVisible else {
return InvisibleElement.constant
}

let stepperFrame = CGRect(origin: attributes.frame.origin, size: stepper.intrinsicContentSize)
let ids = context.ids.nodeIDs(5, for: stepper)

let builder = UIStepperWireframesBuilder(
wireframeRect: stepperFrame,
cornerRadius: stepper.subviews.first?.layer.cornerRadius ?? 0,
backgroundWireframeID: ids[0],
dividerWireframeID: ids[1],
minusWireframeID: ids[2],
plusHorizontalWireframeID: ids[3],
plusVerticalWireframeID: ids[4],
isMinusEnabled: stepper.value > stepper.minimumValue,
isPlusEnabled: stepper.value < stepper.maximumValue
)
let node = Node(viewAttributes: attributes, wireframesBuilder: builder)
return SpecificElement(subtreeStrategy: .ignore, nodes: [node])
}
}

internal struct UIStepperWireframesBuilder: NodeWireframesBuilder {
let wireframeRect: CGRect
let cornerRadius: CGFloat
let backgroundWireframeID: WireframeID
let dividerWireframeID: WireframeID
let minusWireframeID: WireframeID
let plusHorizontalWireframeID: WireframeID
let plusVerticalWireframeID: WireframeID
let isMinusEnabled: Bool
let isPlusEnabled: Bool

func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] {
let background = builder.createShapeWireframe(
id: backgroundWireframeID,
frame: wireframeRect,
borderColor: nil,
borderWidth: nil,
backgroundColor: SystemColors.tertiarySystemFill,
cornerRadius: cornerRadius
)
let verticalMargin: CGFloat = 6
let divider = builder.createShapeWireframe(
id: dividerWireframeID,
frame: CGRect(
origin: CGPoint(x: 0, y: verticalMargin),
size: CGSize(width: 1, height: wireframeRect.size.height - 2 * verticalMargin)
).putInside(wireframeRect, horizontalAlignment: .center, verticalAlignment: .middle),
backgroundColor: SystemColors.placeholderText
)

let horizontalElementRect = CGRect(origin: .zero, size: CGSize(width: 14, height: 2))
let verticalElementRect = CGRect(origin: .zero, size: CGSize(width: 2, height: 14))
let (leftButtonFrame, rightButtonFrame) = wireframeRect.divided(atDistance: wireframeRect.size.width / 2, from: .minXEdge)
let minus = builder.createShapeWireframe(
id: minusWireframeID,
frame: horizontalElementRect.putInside(leftButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle),
backgroundColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText,
cornerRadius: horizontalElementRect.size.height
)
let plusHorizontal = builder.createShapeWireframe(
id: plusHorizontalWireframeID,
frame: horizontalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle),
backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText,
cornerRadius: horizontalElementRect.size.height
)
let plusVertical = builder.createShapeWireframe(
id: plusVerticalWireframeID,
frame: verticalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle),
backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText,
cornerRadius: verticalElementRect.size.width
)
return [background, divider, minus, plusHorizontal, plusVertical]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ internal let defaultNodeRecorders: [NodeRecorder] = [
UISwitchRecorder(),
UISliderRecorder(),
UISegmentRecorder(),
UIStepperRecorder(),
UINavigationBarRecorder(),
UITabBarRecorder(),
UIPickerViewRecorder(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import XCTest
@testable import DatadogSessionReplay

class UIStepperRecorderTests: XCTestCase {
private let recorder = UIStepperRecorder()
private let stepper = UIStepper()
/// `ViewAttributes` simulating common attributes of switch's `UIView`.
private var viewAttributes: ViewAttributes = .mockAny()

func testWhenStepperIsNotVisible() throws {
// When
viewAttributes = .mock(fixture: .invisible)

// Then
let semantics = try XCTUnwrap(recorder.semantics(of: stepper, with: viewAttributes, in: .mockAny()))
XCTAssertTrue(semantics is InvisibleElement)
}

func testWhenStepperIsVisible() throws {
// Given
stepper.tintColor = .mockRandom()

// When
viewAttributes = .mock(fixture: .visible())

// Then
let semantics = try XCTUnwrap(recorder.semantics(of: stepper, with: viewAttributes, in: .mockAny()))
XCTAssertTrue(semantics is SpecificElement)
XCTAssertEqual(semantics.subtreeStrategy, .ignore, "Stepper's subtree should not be recorded")

let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIStepperWireframesBuilder)
}

func testWhenViewIsNotOfExpectedType() {
// When
let view = UITextField()

// Then
XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny()))
}
}

0 comments on commit 9c78109

Please sign in to comment.