From 1320845622b74f6e1981b79979a3653a727af443 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 8 Jan 2025 18:24:19 +0100 Subject: [PATCH] iOS: handle RCTParagraphComponentView components in new arch --- .../DdSessionReplayImplementation.swift | 16 ++- .../ios/Sources/RCTFabricWrapper.h | 13 ++ .../ios/Sources/RCTFabricWrapper.mm | 106 ++++++++++++++++ .../ios/Sources/RCTTextPropertiesWrapper.h | 23 ++++ .../ios/Sources/RCTTextPropertiesWrapper.mm | 28 +++++ .../ios/Sources/RCTTextViewRecorder.swift | 118 ++++++++++-------- .../ios/Tests/DdSessionReplayTests.swift | 27 +++- .../ios/Tests/RCTTextViewRecorderTests.swift | 21 +++- 8 files changed, 292 insertions(+), 60 deletions(-) create mode 100644 packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.h create mode 100644 packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm create mode 100644 packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.h create mode 100644 packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.mm diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index ffa627d79..e576cb0ae 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -15,17 +15,24 @@ public class DdSessionReplayImplementation: NSObject { private lazy var sessionReplay: SessionReplayProtocol = sessionReplayProvider() private let sessionReplayProvider: () -> SessionReplayProtocol private let uiManager: RCTUIManager + private let fabricWrapper: RCTFabricWrapper - internal init(sessionReplayProvider: @escaping () -> SessionReplayProtocol, uiManager: RCTUIManager) { + internal init( + sessionReplayProvider: @escaping () -> SessionReplayProtocol, + uiManager: RCTUIManager, + fabricWrapper: RCTFabricWrapper + ) { self.sessionReplayProvider = sessionReplayProvider self.uiManager = uiManager + self.fabricWrapper = fabricWrapper } @objc public convenience init(bridge: RCTBridge) { self.init( sessionReplayProvider: { NativeSessionReplay() }, - uiManager: bridge.uiManager + uiManager: bridge.uiManager, + fabricWrapper: RCTFabricWrapper() ) } @@ -44,6 +51,7 @@ public class DdSessionReplayImplementation: NSObject { if (customEndpoint != "") { customEndpointURL = URL(string: "\(customEndpoint)/api/v2/replay" as String) } + var sessionReplayConfiguration = SessionReplay.Configuration( replaySampleRate: Float(replaySampleRate), textAndInputPrivacyLevel: convertTextAndInputPrivacy(textAndInputPrivacyLevel), @@ -53,7 +61,9 @@ public class DdSessionReplayImplementation: NSObject { customEndpoint: customEndpointURL ) - sessionReplayConfiguration.setAdditionalNodeRecorders([RCTTextViewRecorder(uiManager: self.uiManager)]) + sessionReplayConfiguration.setAdditionalNodeRecorders([ + RCTTextViewRecorder(uiManager: uiManager, fabricWrapper: fabricWrapper) + ]) if let core = DatadogSDKWrapper.shared.getCoreInstance() { sessionReplay.enable( diff --git a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.h b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.h new file mode 100644 index 000000000..974ff362a --- /dev/null +++ b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.h @@ -0,0 +1,13 @@ +/* + * 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 2016-Present Datadog, Inc. + */ +#import +#import "RCTTextPropertiesWrapper.h" + +@interface RCTFabricWrapper : NSObject + +- (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView* _Nonnull)view; + +@end diff --git a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm new file mode 100644 index 000000000..2f2d5e52c --- /dev/null +++ b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm @@ -0,0 +1,106 @@ +/* + * 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 2016-Present Datadog, Inc. + */ + +#import "RCTFabricWrapper.h" + +#if RCT_NEW_ARCH_ENABLED +#import +#import +namespace rct = facebook::react; +#endif + +@implementation RCTFabricWrapper +/** + * Extracts the text properties from the given UIView when the view is of type RCTParagraphComponentView, returns nil otherwise. + */ +- (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView *)view { + #if RCT_NEW_ARCH_ENABLED + if (![view isKindOfClass:[RCTParagraphComponentView class]]) { + return nil; + } + + // Cast view to RCTParagraphComponentView + RCTParagraphComponentView* paragraphComponentView = (RCTParagraphComponentView *)view; + if (paragraphComponentView == nil) { + return nil; + } + + // Retrieve ParagraphProps from shared pointer + const rct::ParagraphProps* props = (rct::ParagraphProps*)paragraphComponentView.props.get(); + if (props == nil) { + return nil; + } + + // Extract Attributes + RCTTextPropertiesWrapper* textPropertiesWrapper = [[RCTTextPropertiesWrapper alloc] init]; + textPropertiesWrapper.text = [RCTFabricWrapper getTextFromView:paragraphComponentView]; + textPropertiesWrapper.contentRect = paragraphComponentView.bounds; + + rct::TextAttributes textAttributes = props->textAttributes; + textPropertiesWrapper.alignment = [RCTFabricWrapper getAlignmentFromAttributes:textAttributes]; + textPropertiesWrapper.foregroundColor = [RCTFabricWrapper getForegroundColorFromAttributes:textAttributes]; + textPropertiesWrapper.fontSize = [RCTFabricWrapper getFontSizeFromAttributes:textAttributes]; + + return textPropertiesWrapper; + #else + return nil; + #endif +} + +#if RCT_NEW_ARCH_ENABLED ++ (NSString* _Nonnull)getTextFromView:(RCTParagraphComponentView*)view { + if (view == nil || view.attributedText == nil) { + return RCTTextPropertiesDefaultText; + } + + return view.attributedText.string; +} + ++ (NSTextAlignment)getAlignmentFromAttributes:(rct::TextAttributes)textAttributes { + const rct::TextAlignment alignment = textAttributes.alignment.has_value() ? + textAttributes.alignment.value() : + rct::TextAlignment::Natural; + + switch (alignment) { + case rct::TextAlignment::Natural: + return NSTextAlignmentNatural; + + case rct::TextAlignment::Left: + return NSTextAlignmentLeft; + + case rct::TextAlignment::Center: + return NSTextAlignmentCenter; + + case rct::TextAlignment::Right: + return NSTextAlignmentRight; + + case rct::TextAlignment::Justified: + return NSTextAlignmentJustified; + + default: + return RCTTextPropertiesDefaultAlignment; + } +} + ++ (UIColor* _Nonnull)getForegroundColorFromAttributes:(rct::TextAttributes)textAttributes { + @try { + rct::Color color = *textAttributes.foregroundColor; + UIColor* uiColor = (__bridge UIColor*)color.getUIColor().get(); + if (uiColor != nil) { + return uiColor; + } + } @catch (NSException *exception) {} + + return RCTTextPropertiesDefaultForegroundColor; +} + ++ (CGFloat)getFontSizeFromAttributes:(rct::TextAttributes)textAttributes { + // Float is just an alias for CGFloat, but this could change in the future. + _Static_assert(sizeof(rct::Float) == sizeof(CGFloat), "Float and CGFloat are expected to have the same size."); + return isnan(textAttributes.fontSize) ? RCTTextPropertiesDefaultFontSize : (CGFloat)textAttributes.fontSize; +} +#endif +@end diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.h b/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.h new file mode 100644 index 000000000..1701a25e6 --- /dev/null +++ b/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.h @@ -0,0 +1,23 @@ +/* + * 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 2016-Present Datadog, Inc. + */ + +@interface RCTTextPropertiesWrapper : NSObject + +extern NSString* const RCTTextPropertiesDefaultText; +extern NSTextAlignment const RCTTextPropertiesDefaultAlignment; +extern UIColor* const RCTTextPropertiesDefaultForegroundColor; +extern CGFloat const RCTTextPropertiesDefaultFontSize; +extern CGRect const RCTTextPropertiesDefaultContentRect; + +@property (nonatomic, strong, nonnull) NSString* text; +@property (nonatomic, assign) NSTextAlignment alignment; +@property (nonatomic, strong, nonnull) UIColor* foregroundColor; +@property (nonatomic, assign) CGFloat fontSize; +@property (nonatomic, assign) CGRect contentRect; + +- (instancetype _Nonnull) init; + +@end diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.mm b/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.mm new file mode 100644 index 000000000..d4dc4c3a5 --- /dev/null +++ b/packages/react-native-session-replay/ios/Sources/RCTTextPropertiesWrapper.mm @@ -0,0 +1,28 @@ +/* + * 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 2016-Present Datadog, Inc. + */ +#import "RCTTextPropertiesWrapper.h" + +@implementation RCTTextPropertiesWrapper + +NSString* const RCTTextPropertiesDefaultText = @""; +NSTextAlignment const RCTTextPropertiesDefaultAlignment = NSTextAlignmentNatural; +UIColor* const RCTTextPropertiesDefaultForegroundColor = [UIColor blackColor]; +CGFloat const RCTTextPropertiesDefaultFontSize = 14.0; +CGRect const RCTTextPropertiesDefaultContentRect = CGRectZero; + +- (instancetype)init { + self = [super init]; + if (self) { + _text = RCTTextPropertiesDefaultText; + _alignment = RCTTextPropertiesDefaultAlignment; + _foregroundColor = RCTTextPropertiesDefaultForegroundColor; + _fontSize = RCTTextPropertiesDefaultFontSize; + _contentRect = RCTTextPropertiesDefaultContentRect; + } + return self; +} + +@end diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 70682c69c..c2d354592 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -17,27 +17,11 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { internal var identifier = UUID() internal let uiManager: RCTUIManager + internal let fabricWrapper: RCTFabricWrapper - internal init(uiManager: RCTUIManager) { + internal init(uiManager: RCTUIManager, fabricWrapper: RCTFabricWrapper) { self.uiManager = uiManager - } - - internal func extractTextFromSubViews( - subviews: [RCTShadowView]? - ) -> String? { - if let subviews = subviews { - return subviews.compactMap { subview in - if let sub = subview as? RCTRawTextShadowView { - return sub.text - } - if let sub = subview as? RCTVirtualTextShadowView { - // We recursively get all subviews for nested Text components - return extractTextFromSubViews(subviews: sub.reactSubviews()) - } - return nil - }.joined() - } - return nil + self.fabricWrapper = fabricWrapper } public func semantics( @@ -45,6 +29,48 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { with attributes: SessionReplayViewAttributes, in context: SessionReplayViewTreeRecordingContext ) -> SessionReplayNodeSemantics? { + guard + let textProperties = fabricWrapper.tryToExtractTextProperties(from: view) ?? tryToExtractTextProperties(view: view) + else { + return view is RCTTextView ? SessionReplayInvisibleElement.constant : nil + } + + let builder = RCTTextViewWireframesBuilder( + wireframeID: context.ids.nodeID(view: view, nodeRecorder: self), + attributes: attributes, + text: textProperties.text, + textAlignment: textProperties.alignment, + textColor: textProperties.foregroundColor, + textObfuscator: textObfuscator(context), + fontSize: textProperties.fontSize, + contentRect: textProperties.contentRect + ) + + return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [ + SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder) + ]) + } + + internal func tryToExtractTextFromSubViews( + subviews: [RCTShadowView]? + ) -> String? { + guard let subviews = subviews else { + return nil + } + + return subviews.compactMap { subview in + if let sub = subview as? RCTRawTextShadowView { + return sub.text + } + if let sub = subview as? RCTVirtualTextShadowView { + // We recursively get all subviews for nested Text components + return tryToExtractTextFromSubViews(subviews: sub.reactSubviews()) + } + return nil + }.joined() + } + + private func tryToExtractTextProperties(view: UIView) -> RCTTextPropertiesWrapper? { guard let textView = view as? RCTTextView else { return nil } @@ -56,41 +82,35 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { shadowView = uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView } - if let shadow = shadowView { - // TODO: RUM-2173 check performance is ok - let text = extractTextFromSubViews( - subviews: shadow.reactSubviews() - ) + guard let shadow = shadowView else { + return nil + } - let builder = RCTTextViewWireframesBuilder( - wireframeID: context.ids.nodeID(view: textView, nodeRecorder: self), - attributes: attributes, - text: text, - textAlignment: shadow.textAttributes.alignment, - textColor: shadow.textAttributes.foregroundColor?.cgColor, - textObfuscator: textObfuscator(context), - fontSize: shadow.textAttributes.fontSize, - contentRect: shadow.contentFrame - ) - let node = SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder) - return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [node]) + let textProperties = RCTTextPropertiesWrapper() + + // TODO: RUM-2173 check performance is ok + if let text = tryToExtractTextFromSubViews(subviews: shadow.reactSubviews()) { + textProperties.text = text } - return SessionReplayInvisibleElement.constant - } -} -// Black color. This is the default for RN: https://github.com/facebook/react-native/blob/a5ee029cd02a636136058d82919480eeeb700067/packages/react-native/Libraries/Text/RCTTextAttributes.mm#L250 -let DEFAULT_COLOR = UIColor.black.cgColor + if let foregroundColor = shadow.textAttributes.foregroundColor { + textProperties.foregroundColor = foregroundColor + } -// Default font size for RN: https://github.com/facebook/react-native/blob/16dff523b0a16d7fa9b651062c386885c2f48a6b/packages/react-native/React/Views/RCTFont.mm#L396 -let DEFAULT_FONT_SIZE = CGFloat(14) + textProperties.alignment = shadow.textAttributes.alignment + textProperties.fontSize = shadow.textAttributes.fontSize + textProperties.contentRect = shadow.contentFrame + + return textProperties + } +} internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder { let wireframeID: WireframeID let attributes: SessionReplayViewAttributes - let text: String? + let text: String var textAlignment: NSTextAlignment - let textColor: CGColor? + let textColor: UIColor let textObfuscator: SessionReplayTextObfuscating let fontSize: CGFloat let contentRect: CGRect @@ -140,12 +160,12 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder id: wireframeID, frame: relativeIntersectedRect, clip: attributes.clip, - text: textObfuscator.mask(text: text ?? ""), + text: textObfuscator.mask(text: text), textFrame: textFrame, - // Text alignment is top for all RCTTextView components. + // Text alignment is top for all RCTTextView and RCTParagraphComponentView components. textAlignment: .init(systemTextAlignment: textAlignment, vertical: .top), - textColor: textColor ?? DEFAULT_COLOR, - fontOverride: SessionReplayWireframesBuilder.FontOverride(size: fontSize.isNaN ? DEFAULT_FONT_SIZE : fontSize), + textColor: textColor.cgColor, + fontOverride: SessionReplayWireframesBuilder.FontOverride(size: fontSize.isNaN ? RCTTextPropertiesDefaultFontSize : fontSize), borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, backgroundColor: attributes.backgroundColor, diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index 6626d93c7..db6910502 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -41,6 +41,7 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithZeroReplaySampleRate() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() + let fabricWrapperMock = MockFabricWrapper() guard let imagePrivacyLevel = imagePrivacyMap.keys.randomElement(), @@ -54,7 +55,11 @@ internal class DdSessionReplayTests: XCTestCase { return } - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( + DdSessionReplayImplementation( + sessionReplayProvider:{ sessionReplayMock }, + uiManager: uiManagerMock, + fabricWrapper: fabricWrapperMock + ).enable( replaySampleRate: 0, customEndpoint: "", imagePrivacyLevel: NSString(string: imagePrivacyLevel), @@ -77,8 +82,13 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithBadPrivacyLevels() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() + let fabricWrapperMock = MockFabricWrapper() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( + DdSessionReplayImplementation( + sessionReplayProvider:{ sessionReplayMock }, + uiManager: uiManagerMock, + fabricWrapper: fabricWrapperMock + ).enable( replaySampleRate: 100, customEndpoint: "", imagePrivacyLevel: "BAD_VALUE", @@ -101,6 +111,7 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithCustomEndpoint() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() + let fabricWrapperMock = MockFabricWrapper() guard let imagePrivacyLevel = imagePrivacyMap.keys.randomElement(), @@ -114,7 +125,11 @@ internal class DdSessionReplayTests: XCTestCase { return } - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( + DdSessionReplayImplementation( + sessionReplayProvider:{ sessionReplayMock }, + uiManager: uiManagerMock, + fabricWrapper: fabricWrapperMock + ).enable( replaySampleRate: 100, customEndpoint: "https://session-replay.example.com", imagePrivacyLevel: NSString(string: imagePrivacyLevel), @@ -175,6 +190,12 @@ private class MockSessionReplay: SessionReplayProtocol { private class MockUIManager: RCTUIManager {} +private class MockFabricWrapper: RCTFabricWrapper { + override func tryToExtractTextProperties(from view: UIView) -> RCTTextPropertiesWrapper? { + return nil + } +} + private class MockDatadogCore: DatadogCoreProtocol { func mostRecentModifiedFileAt(before: Date) throws -> Date? { return nil diff --git a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift index 5a200801c..d48977f5b 100644 --- a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift +++ b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift @@ -83,7 +83,8 @@ internal class RCTTextViewRecorderTests: XCTestCase { func testReturnsNilIfViewIsNotRCTTextView() { let viewMock = UIView() let uiManagerMock = MockUIManager() - let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + let fabricWrapperMock = MockFabricWrapper() + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock, fabricWrapper: fabricWrapperMock) let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) @@ -93,9 +94,10 @@ internal class RCTTextViewRecorderTests: XCTestCase { func testReturnsInvisibleElementIfShadowViewIsNotFound() throws { let reactTag = NSNumber(value: 44) let uiManagerMock = MockUIManager() + let fabricWrapperMock = MockFabricWrapper() let viewMock = RCTTextView() viewMock.reactTag = reactTag - let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock, fabricWrapper: fabricWrapperMock) let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) @@ -106,9 +108,10 @@ internal class RCTTextViewRecorderTests: XCTestCase { func testReturnsBuilderWithCorrectInformation() throws { let reactTag = NSNumber(value: 44) let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView) + let fabricWrapperMock = MockFabricWrapper() let viewMock = RCTTextView() viewMock.reactTag = reactTag - let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock, fabricWrapper: fabricWrapperMock) let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) @@ -135,9 +138,10 @@ internal class RCTTextViewRecorderTests: XCTestCase { func testReturnsBuilderWithCorrectInformationWhenNestedTextComponents() throws { let reactTag = NSNumber(value: 44) let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowViewNestedText) + let fabricWrapperMock = MockFabricWrapper() let viewMock = RCTTextView() viewMock.reactTag = reactTag - let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock, fabricWrapper: fabricWrapperMock) let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) @@ -168,9 +172,10 @@ internal class RCTTextViewRecorderTests: XCTestCase { ) let reactTag = NSNumber(value: 44) let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView) + let fabricWrapperMock = MockFabricWrapper() let viewMock = RCTTextView() viewMock.reactTag = reactTag - let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock, fabricWrapper: fabricWrapperMock) let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockMaskContext) @@ -204,6 +209,12 @@ private class MockUIManager: RCTUIManager { } +private class MockFabricWrapper: RCTFabricWrapper { + override func tryToExtractTextProperties(from view: UIView) -> RCTTextPropertiesWrapper? { + return nil + } +} + extension SessionReplayInvisibleElement: Equatable { public static func ==(lhs: SessionReplayInvisibleElement, rhs: SessionReplayInvisibleElement) -> Bool { // If two elements are indeed InvisibleElement they're InvisibleElement.constant