From 4e0d0c81b3dcc03b3070bd45ba0328172341d46f Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 30 Nov 2023 17:05:09 +0100 Subject: [PATCH 1/2] Fix iOS SR Text with padding --- .../ios/Sources/RCTTextViewRecorder.swift | 24 +++++++++++---- .../ios/Tests/RCTTextViewRecorderTests.swift | 29 +++++++++++++++---- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 5a6db8466..50e52a577 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -95,11 +95,13 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder let DEFAULT_FONT_COLOR = UIColor.black.cgColor + // Clipping should be 0 to avoid the text from overflowing when the + // numberOfLines prop is used. private var clip: SRContentClip { - let top = abs(contentRect.origin.y) - let left = abs(contentRect.origin.x) - let bottom = max(contentRect.height - attributes.frame.height - top, 0) - let right = max(contentRect.width - attributes.frame.width - left, 0) + let top = 0.0 + let left = 0.0 + let bottom = 0.0 + let right = 0.0 return SRContentClip.create( bottom: Int64(withNoOverflow: bottom), left: Int64(withNoOverflow: left), @@ -110,19 +112,29 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder private var relativeIntersectedRect: CGRect { return CGRect( - x: attributes.frame.origin.x - contentRect.origin.x, - y: attributes.frame.origin.y - contentRect.origin.y , + x: attributes.frame.origin.x, + y: attributes.frame.origin.y, width: max(contentRect.width, attributes.frame.width), height: max(contentRect.height, attributes.frame.height) ) } + private var textFrame: CGRect { + return CGRect( + x: attributes.frame.origin.x + contentRect.origin.x, + y: attributes.frame.origin.y + contentRect.origin.y, + width: contentRect.width, + height: contentRect.height + ) + } + public func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [SRWireframe] { return [ builder.createTextWireframe( id: wireframeID, frame: relativeIntersectedRect, text: textObfuscator.mask(text: text ?? ""), + textFrame: textFrame, textAlignment: .init(systemTextAlignment: textAlignment, vertical: .center), clip: clip, textColor: textColor ?? DEFAULT_FONT_COLOR, diff --git a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift index 852fde475..99720080f 100644 --- a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift +++ b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift @@ -10,9 +10,13 @@ import XCTest @testable import DatadogSessionReplay import React +let BACKGROUND_RECT = CGRect(x: 50, y: 50, width: 100, height: 100) +// Simulates view with padding vertical of 10 and horizontal of 20. +let INNER_TEXT_RECT = CGRect(x: 20, y: 10, width: 60, height: 80) // position inside the background view + internal class RCTTextViewRecorderTests: XCTestCase { let mockAttributes = SessionReplayViewAttributes( - frame: CGRect(x: 0, y: 0, width: 100, height: 100), + frame: BACKGROUND_RECT, backgroundColor: UIColor.white.cgColor, layerBorderColor: UIColor.blue.cgColor, layerBorderWidth: CGFloat(1.0), @@ -21,14 +25,14 @@ internal class RCTTextViewRecorderTests: XCTestCase { isHidden: false, intrinsicContentSize: CGSize(width: 100.0, height: 100.0) ) - + let mockAllowContext = SessionReplayViewTreeRecordingContext( recorder: .init(privacy: SessionReplayPrivacyLevel.allow, applicationID: "app_id", sessionID: "session_id", viewID: "view_id", viewServerTimeOffset: nil), coordinateSpace: UIView(), ids: .init(), imageDataProvider: ImageDataProvider() ) - + var mockShadowView: RCTTextShadowView { // The shadow view must be initialized with a bridge so that we can insert React Subviews into it. let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none)); @@ -37,9 +41,11 @@ internal class RCTTextViewRecorderTests: XCTestCase { rawTextShadowView.text = "This is the test text." shadowView.insertReactSubview(rawTextShadowView, at: 0) + shadowView.layoutMetrics.contentFrame = INNER_TEXT_RECT + return shadowView } - + var mockShadowViewNestedText: RCTTextShadowView { // The shadow view must be initialized with a bridge so that we can insert React Subviews into it. let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none)); @@ -79,7 +85,7 @@ internal class RCTTextViewRecorderTests: XCTestCase { let element = try XCTUnwrap(result as? SessionReplayInvisibleElement) XCTAssertEqual(element, SessionReplayInvisibleElement.constant) } - + func testReturnsBuilderWithCorrectInformation() throws { let reactTag = NSNumber(value: 44) let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView) @@ -94,6 +100,19 @@ internal class RCTTextViewRecorderTests: XCTestCase { XCTAssertEqual(element.nodes.count, 1) let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe()) XCTAssertEqual(wireframe.text, "This is the test text.") + + // Wireframe represents the background box. + XCTAssertEqual(wireframe.height, 100) + XCTAssertEqual(wireframe.width, 100) + XCTAssertEqual(wireframe.x, 50) + XCTAssertEqual(wireframe.y, 50) + + // Padding around the test box in the background box. + XCTAssertEqual(wireframe.textPosition?.padding, .init( + bottom: Int64(BACKGROUND_RECT.height - INNER_TEXT_RECT.height - INNER_TEXT_RECT.minY), + left: Int64(INNER_TEXT_RECT.minX), + right: Int64(BACKGROUND_RECT.width - INNER_TEXT_RECT.width - INNER_TEXT_RECT.minX), + top: Int64(INNER_TEXT_RECT.minY))) } func testReturnsBuilderWithCorrectInformationWhenNestedTextComponents() throws { From 5e1e7202940c67a2a5382beff0ff4852acd2f760 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 1 Dec 2023 10:21:16 +0100 Subject: [PATCH 2/2] Vertically align texts to top in SR --- .../ios/Sources/RCTTextViewRecorder.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 50e52a577..b2317bee6 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -135,7 +135,8 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder frame: relativeIntersectedRect, text: textObfuscator.mask(text: text ?? ""), textFrame: textFrame, - textAlignment: .init(systemTextAlignment: textAlignment, vertical: .center), + // Text alignment is top for all RCTTextView components. + textAlignment: .init(systemTextAlignment: textAlignment, vertical: .top), clip: clip, textColor: textColor ?? DEFAULT_FONT_COLOR, font: font,