diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index daa0f67689..4071332dc0 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -829,6 +829,7 @@ D4CBA2532DE06D1600581618 /* TestConstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CBA2512DE06D1600581618 /* TestConstantTests.swift */; }; D4CD2A802DE9F91900DA9F59 /* SentryRedactRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CD2A7C2DE9F91900DA9F59 /* SentryRedactRegion.swift */; }; D4CD2A812DE9F91900DA9F59 /* SentryRedactRegionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CD2A7D2DE9F91900DA9F59 /* SentryRedactRegionType.swift */; }; + D4CD38AD2E9F946400585285 /* SentryUIRedactBuilderTests+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CD38AC2E9F946400585285 /* SentryUIRedactBuilderTests+SwiftUI.swift */; }; D4D12E7A2DFC608800DC45C4 /* SentryScreenshotOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */; }; D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; @@ -2162,6 +2163,7 @@ D4CBA2512DE06D1600581618 /* TestConstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstantTests.swift; sourceTree = ""; }; D4CD2A7C2DE9F91900DA9F59 /* SentryRedactRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactRegion.swift; sourceTree = ""; }; D4CD2A7D2DE9F91900DA9F59 /* SentryRedactRegionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactRegionType.swift; sourceTree = ""; }; + D4CD38AC2E9F946400585285 /* SentryUIRedactBuilderTests+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryUIRedactBuilderTests+SwiftUI.swift"; sourceTree = ""; }; D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptionsTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDummyPrivateEmptyClass.m; sourceTree = ""; }; @@ -4197,6 +4199,7 @@ D8F67AF22BE10F7600C9197B /* SentryUIRedactBuilderTests+Common.swift */, D4AF7D2B2E9404ED004F0F59 /* SentryUIRedactBuilderTests+EdgeCases.swift */, D4AF7D272E9402AC004F0F59 /* SentryUIRedactBuilderTests+SpecialViews.swift */, + D4CD38AC2E9F946400585285 /* SentryUIRedactBuilderTests+SwiftUI.swift */, D4AF7D252E9401EB004F0F59 /* SentryUIRedactBuilderTests+UIKit.swift */, D4AF7D212E93FFCA004F0F59 /* SentryUIRedactBuilderTests+ReactNative.swift */, D45E2D762E003EBF0072A6B7 /* TestRedactOptions.swift */, @@ -6204,6 +6207,7 @@ 630C01941EC3402C00C52CEF /* SentryCrashReportConverterTests.m in Sources */, 7B59398424AB481B0003AAD2 /* NotificationCenterTestCase.swift in Sources */, 7B0A542E2521C62400A71716 /* SentryFrameRemoverTests.swift in Sources */, + D4CD38AD2E9F946400585285 /* SentryUIRedactBuilderTests+SwiftUI.swift in Sources */, 7BE912B12721C76000E49E62 /* SentryPerformanceTrackingIntegrationTests.swift in Sources */, 7BA61CCC247D14E600C130A8 /* SentryDefaultThreadInspectorTests.swift in Sources */, 623C45B02A651D8200D9E88B /* SentryCoreDataTracker+Test.m in Sources */, diff --git a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift new file mode 100644 index 0000000000..e014da3e6d --- /dev/null +++ b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift @@ -0,0 +1,930 @@ +#if os(iOS) +import AVKit +import Foundation +import PDFKit +import SafariServices +@_spi(Private) @testable import Sentry +import SentryTestUtils +import SnapshotTesting +import SwiftUI +import UIKit +import WebKit +import XCTest + +// The following command was used to derive the view hierarchy: +// +// ``` +// (lldb) po rootView.value(forKey: "recursiveDescription")! +// ``` +class SentryUIRedactBuilderTests_SwiftUI: SentryUIRedactBuilderTests { // swiftlint:disable:this type_name + private func getSut(maskAllText: Bool, maskAllImages: Bool, maskedViewClasses: [AnyClass] = []) -> SentryUIRedactBuilder { + return SentryUIRedactBuilder(options: TestRedactOptions( + maskAllText: maskAllText, + maskAllImages: maskAllImages, + maskedViewClasses: maskedViewClasses + )) + } + + // MARK: - SwiftUI.Text Redaction + + private func setupSwiftUITextFixture() throws -> UIWindow { + let view = VStack { + VStack { + Text("Hello SwiftUI") + .padding(20) + } + .background(Color.green) + .font(.system(size: 20)) // Use a fixed font size as defaults could change frame + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 0, y: 0, width: 250, height: 250)) + + // View Hierarchy: + // --------------- + // == iOS 26 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGS1_GS2_VS_4TextVS_14_PaddingLayout__GVS_24_BackgroundStyleModifierVS_5Color____: 0x10900bc00; frame = (0 0; 120 60); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | (layer) + // | | | | <_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer: 0x600002c21e80> (layer) + // + // == iOS 18 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGS1_GS2_VS_4TextVS_14_PaddingLayout__GVS_24_BackgroundStyleModifierVS_5Color____: 0x104c27a00; frame = (0 0; 120 60); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + // | | | | > + // + // == iOS 17 & 16 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGS2_GS1_GS2_VS_4TextVS_14_PaddingLayout__GVS_24_BackgroundStyleModifierVS_5Color__GVS_30_EnvironmentKeyWritingModifierGSqVS_4Font_____: 0x14781e800; frame = (0 0; 250 250); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + // | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x141204280; frame = (68.6667 142.667; 112.667 24); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8PlatformP33_65A81BD07F0108B0485D2E15DE104A7514CGDrawingLayer: 0x600000228000>> + } + + private func assertSwiftUITextRegions(regions: [SentryRedactRegion]) throws { + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.type, .redact) + XCTAssertCGSizeEqual(region.size, CGSize(width: 112.666, height: 24), accuracy: 0.01) + if #available(iOS 18, *) { + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 68.666, ty: 144), + accuracy: 0.01 + ) + } else { + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 68.666, ty: 142.666), + accuracy: 0.01 + ) + } + + let region2 = try XCTUnwrap(regions.element(at: 1)) + XCTAssertNil(region2.color) + XCTAssertEqual(region2.type, .clipOut) + XCTAssertCGSizeEqual(region.size, CGSize(width: 112.666, height: 24), accuracy: 0.01) + if #available(iOS 18, *) { + XCTAssertAffineTransformEqual( + region2.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 48.666, ty: 124), + accuracy: 0.01 + ) + } else { + XCTAssertAffineTransformEqual( + region2.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 48.666, ty: 122.666), + accuracy: 0.01 + ) + } + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 2) + } + + func testRedact_withSwiftUIText_withMaskAllTextEnabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUITextFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUITextRegions(regions: result) + } + + func testRedact_withSwiftUIText_withMaskAllTextDisabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUITextFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image) + assertSnapshot(of: masked, as: .image) + + let region = try XCTUnwrap(result.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertEqual(region.type, .clipOut) + XCTAssertCGSizeEqual(region.size, CGSize(width: 152.666, height: 64), accuracy: 0.01) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 48.666, ty: 122.666), + accuracy: 0.01 + ) + + // Assert no other regions + XCTAssertEqual(result.count, 1) + } + + func testRedact_withSwiftUIText_withMaskAllImagesDisabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUITextFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image) + assertSnapshot(of: masked, as: .image) + try assertSwiftUITextRegions(regions: result) + } + + // MARK: - SwiftUI.Label Redaction + + private func setupSwiftUILabelFixture() throws -> UIWindow { + let view = VStack { + Label("Hello SwiftUI", systemImage: "house") + .labelStyle(.titleAndIcon) + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 0, y: 0, width: 300, height: 300)) + + // View Hierarchy: + // --------------- + // == iOS 26 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGVS_5LabelVS_4TextVS_5Image_GVS_P10$1d976f51025LabelStyleWritingModifierVS_22TitleAndIconLabelStyle____: 0x107853850; frame = (0 0; 120 60); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | (layer) + // | | | | <_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer: 0x600002c26680> (layer) + // + // == iOS 18 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGVS_5LabelVS_4TextVS_5Image_GVS_P10$1d433610c25LabelStyleWritingModifierVS_22TitleAndIconLabelStyle____: 0x1049414e0; frame = (0 0; 120 60); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + // | | | | > + // + // == iOS 17 & 16 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_15ModifiedContentGVS_5LabelVS_4TextVS_5Image_GVS_P10$11e37ba8025LabelStyleWritingModifierVS_22TitleAndIconLabelStyle____: 0x130524b10; frame = (0 0; 300 300); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + // | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x14073f040; frame = (117 169.333; 98 20.3333); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8PlatformP33_65A81BD07F0108B0485D2E15DE104A7514CGDrawingLayer: 0x6000028e1020>> + } + + private func assertSwiftUILabelRegions(regions: [SentryRedactRegion], expectText: Bool, expectImage: Bool) throws { + func assertTextRegion(region: SentryRedactRegion) { + XCTAssertNil(region.color) + XCTAssertEqual(region.type, .redact) + XCTAssertCGSizeEqual(region.size, CGSize(width: 98, height: 20.333), accuracy: 0.01) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 117, ty: 169.333), + accuracy: 0.01 + ) + } + + func assertImageRegion(region: SentryRedactRegion) { + XCTAssertNil(region.color) + XCTAssertEqual(region.type, .redact) + XCTAssertCGSizeEqual(region.size, CGSize(width: 20, height: 17.666), accuracy: 0.01) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 87, ty: 170.333), + accuracy: 0.01 + ) + } + + if expectText && expectImage { + assertTextRegion(region: try XCTUnwrap(regions.element(at: 0))) + assertImageRegion(region: try XCTUnwrap(regions.element(at: 1))) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 2) + } else if expectText { + assertTextRegion(region: try XCTUnwrap(regions.element(at: 0))) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } else if expectImage { + assertImageRegion(region: try XCTUnwrap(regions.element(at: 0))) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } else { + // Assert that there are no other regions + XCTAssertEqual(regions.count, 0) + } + } + + @available(iOS 14.5, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + func testRedact_withSwiftUILabel_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactTextAndImage() throws { + // -- Arrange -- + let window = try setupSwiftUILabelFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUILabelRegions(regions: result, expectText: true, expectImage: true) + } + + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + func testRedact_withSwiftUILabel_withMaskAllTextEnabled_withMaskAllImagesDisabled_shouldRedactText() throws { + // -- Arrange -- + let window = try setupSwiftUILabelFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUILabelRegions(regions: result, expectText: true, expectImage: false) + } + + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + func testRedact_withSwiftUILabel_withMaskAllTextDisabled_withMaskAllImagesEnabled_shouldRedactImage() throws { + // -- Arrange -- + let window = try setupSwiftUILabelFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUILabelRegions(regions: result, expectText: false, expectImage: true) + } + + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + func testRedact_withSwiftUILabel_withMaskAllTextDisabled_withMaskAllImagesDisabled_shouldRedactText() throws { + // -- Arrange -- + let window = try setupSwiftUILabelFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUILabelRegions(regions: result, expectText: false, expectImage: false) + } + + // MARK: - SwiftUI.List Redaction + + private func setupSwiftUIListFixture() throws -> UIWindow { + let view = VStack { + List { + Section("Section 1") { + Text("Item 1") + } + Section { + Text("Item 2") + } + } + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 0, y: 0, width: 300, height: 500)) + + // View Hierarchy: + // --------------- + // === 16 === + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_4ListOs5NeverGVS_9TupleViewTGVS_7SectionVS_4TextS6_VS_9EmptyView_GS5_S7_S6_S7_______: 0x12f811200; frame = (0 0; 300 500); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | <_TtGC7SwiftUI16PlatformViewHostGVS_P10$111818dc817ListRepresentableGVS_28CollectionViewListDataSourceOs5Never_GOS_19SelectionManagerBoxS3____: 0x12f514910; baseClass = _UIConstraintBasedLayoutHostingView; frame = (0 0; 300 500); anchorPoint = (0, 0); tintColor = UIExtendedSRGBColorSpace 0 0.478431 1 1; layer = > + // | | | | | ; backgroundColor = ; layer = ; contentOffset: {0, -59}; contentSize: {300, 182}; adjustedContentInset: {59, 0, 0, 0}; layout: ; dataSource: <_TtGC7SwiftUI31UICollectionViewListCoordinatorGVS_28CollectionViewListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x15f549e70>> + // | | | | | | <_UICollectionViewListLayoutSectionBackgroundColorDecorationView: 0x12f514bc0; frame = (-16 -1000; 332 1100.33); userInteractionEnabled = NO; backgroundColor = ; layer = > + // | | | | | | <_UICollectionViewListLayoutSectionBackgroundColorDecorationView: 0x12f512940; frame = (-16 100.333; 332 1081.67); userInteractionEnabled = NO; backgroundColor = ; layer = > + // | | | | | | > + // | | | | | | | <_UISystemBackgroundView: 0x14f5544e0; frame = (0 0; 268 44); layer = ; configuration = >> + // | | | | | | | | ; layer = > + // | | | | | | | <_UICollectionViewListCellContentView: 0x14f553b80; frame = (0 0; 268 44); gestureRecognizers = ; layer = > + // | | | | | | | | <_TtGC7SwiftUI15CellHostingViewGVS_15ModifiedContentVS_14_ViewList_ViewVS_26CollectionViewCellModifier__: 0x13f81a800; frame = (0 0; 268 44); autoresize = W+H; gestureRecognizers = ; layer = > + // | | | | | | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x13a9053c0; frame = (16 12; 45.3333 20.3333); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8PlatformP33_65A81BD07F0108B0485D2E15DE104A7514CGDrawingLayer: 0x600001500540>> + // | | | | | | > + // | | | | | | | <_UISystemBackgroundView: 0x13f624100; frame = (0 0; 268 44); layer = ; configuration = >> + // | | | | | | | | ; layer = > + // | | | | | | | <_UICollectionViewListCellContentView: 0x13f623da0; frame = (0 0; 268 44); gestureRecognizers = ; layer = > + // | | | | | | | | <_TtGC7SwiftUI15CellHostingViewGVS_15ModifiedContentVS_14_ViewList_ViewVS_26CollectionViewCellModifier__: 0x14802f600; frame = (0 0; 268 44); autoresize = W+H; gestureRecognizers = ; layer = > + // | | | | | | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x13a904cf0; frame = (16 12; 47.6667 20.3333); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8PlatformP33_65A81BD07F0108B0485D2E15DE104A7514CGDrawingLayer: 0x6000015000c0>> + // | | | | | | > + // | | | | | | | <_UISystemBackgroundView: 0x13f6274d0; frame = (0 0; 268 38.6667); layer = ; configuration = > + // | | | | | | | <_UICollectionViewListCellContentView: 0x13f626f00; frame = (0 0; 268 38.6667); gestureRecognizers = ; layer = > + // | | | | | | | | <_TtGC7SwiftUI15CellHostingViewGVS_15ModifiedContentVS_14_ViewList_ViewVS_26CollectionViewCellModifier__: 0x148026000; frame = (0 0; 268 38.6667); autoresize = W+H; gestureRecognizers = ; layer = > + // | | | | | | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x13f521ef0; frame = (16 17; 65.6667 15.6667); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8PlatformP33_65A81BD07F0108B0485D2E15DE104A7514CGDrawingLayer: 0x60000150d020>> + // | | | | | | <_UIScrollViewScrollIndicator: 0x14f525ce0; frame = (294 431; 3 7); alpha = 0; autoresize = LM; layer = > + // | | | | | | | > + // | | | | | | <_UIScrollViewScrollIndicator: 0x14f551a80; frame = (290 435; 7 3); alpha = 0; autoresize = TM; layer = > + // | | | | | | | > + } + + private func assertSwiftUIListRegions(regions: [SentryRedactRegion], expectText: Bool) throws { + var offset = 0 + + let region0 = try XCTUnwrap(regions.element(at: offset + 0)) // clipBegin for main collection view + XCTAssertNil(region0.color) + XCTAssertCGSizeEqual(region0.size, CGSize(width: 300, height: 500), accuracy: 0.01) + XCTAssertEqual(region0.type, .clipBegin) + XCTAssertAffineTransformEqual( + region0.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0), + accuracy: 0.01 + ) + + if expectText { + let region1 = try XCTUnwrap(regions.element(at: offset + 1)) // redact for first cell's text + XCTAssertNil(region1.color) + XCTAssertCGSizeEqual(region1.size, CGSize(width: 65.6667, height: 15.6667), accuracy: 0.01) + XCTAssertEqual(region1.type, .redact) + XCTAssertAffineTransformEqual( + region1.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 32, ty: 76), + accuracy: 0.01 + ) + offset += 1 + } + + let region2 = try XCTUnwrap(regions.element(at: offset + 1)) // clipBegin for second cell + XCTAssertNil(region2.color) + XCTAssertCGSizeEqual(region2.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region2.type, .clipBegin) + XCTAssertAffineTransformEqual( + region2.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 177), + accuracy: 0.01 + ) + + if expectText { + let region3 = try XCTUnwrap(regions.element(at: offset + 2)) // redact for second cell's text + XCTAssertNil(region3.color) + XCTAssertCGSizeEqual(region3.size, CGSize(width: 47.6667, height: 20.3333), accuracy: 0.01) + XCTAssertEqual(region3.type, .redact) + XCTAssertAffineTransformEqual( + region3.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 32, ty: 189), + accuracy: 0.01 + ) + offset += 1 + } + + let region4 = try XCTUnwrap(regions.element(at: offset + 2)) // clipOut for second cell + XCTAssertNil(region4.color) + XCTAssertCGSizeEqual(region4.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region4.type, .clipOut) + XCTAssertAffineTransformEqual( + region4.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 177), + accuracy: 0.01 + ) + + let region5 = try XCTUnwrap(regions.element(at: offset + 3)) // clipEnd for second cell + XCTAssertNil(region5.color) + XCTAssertCGSizeEqual(region5.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region5.type, .clipEnd) + XCTAssertAffineTransformEqual( + region5.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 177), + accuracy: 0.01 + ) + + let region6 = try XCTUnwrap(regions.element(at: offset + 4)) // clipBegin for first cell + XCTAssertNil(region6.color) + XCTAssertCGSizeEqual(region6.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region6.type, .clipBegin) + XCTAssertAffineTransformEqual( + region6.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 97.6667), + accuracy: 0.01 + ) + + if expectText { + let region7 = try XCTUnwrap(regions.element(at: offset + 5)) // redact for first cell's text + XCTAssertNil(region7.color) + XCTAssertCGSizeEqual(region7.size, CGSize(width: 45.3333, height: 20.3333), accuracy: 0.01) + XCTAssertEqual(region7.type, .redact) + XCTAssertAffineTransformEqual( + region7.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 32, ty: 109.6667), + accuracy: 0.01 + ) + offset += 1 + } + + let region8 = try XCTUnwrap(regions.element(at: offset + 5)) // clipOut for first cell + XCTAssertNil(region8.color) + XCTAssertCGSizeEqual(region8.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region8.type, .clipOut) + XCTAssertAffineTransformEqual( + region8.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 97.6667), + accuracy: 0.01 + ) + + let region9 = try XCTUnwrap(regions.element(at: offset + 6)) // clipEnd for first cell + XCTAssertNil(region9.color) + XCTAssertCGSizeEqual(region9.size, CGSize(width: 268, height: 44), accuracy: 0.01) + XCTAssertEqual(region9.type, .clipEnd) + XCTAssertAffineTransformEqual( + region9.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 16, ty: 97.6667), + accuracy: 0.01 + ) + + let region10 = try XCTUnwrap(regions.element(at: offset + 7)) // redact for section background (bottom) + XCTAssertNil(region10.color) + XCTAssertCGSizeEqual(region10.size, CGSize(width: 332, height: 1_081.6667), accuracy: 0.01) + XCTAssertEqual(region10.type, .redact) + XCTAssertAffineTransformEqual( + region10.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: -16, ty: 159.3333), + accuracy: 0.01 + ) + + let region11 = try XCTUnwrap(regions.element(at: offset + 8)) // redact for section background (top) + XCTAssertNil(region11.color) + XCTAssertCGSizeEqual(region11.size, CGSize(width: 332, height: 1_100.3333), accuracy: 0.01) + XCTAssertEqual(region11.type, .redact) + XCTAssertAffineTransformEqual( + region11.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: -16, ty: -941), + accuracy: 0.01 + ) + + let region12 = try XCTUnwrap(regions.element(at: offset + 9)) // clipEnd for main collection view + XCTAssertNil(region12.color) + XCTAssertCGSizeEqual(region12.size, CGSize(width: 300, height: 500), accuracy: 0.01) + XCTAssertEqual(region12.type, .clipEnd) + XCTAssertAffineTransformEqual( + region12.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0), + accuracy: 0.01 + ) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, offset + 10) + } + + func testRedact_withSwiftUIList_withMaskAllTextEnabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIListFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIListRegions(regions: result, expectText: true) + } + + func testRedact_withSwiftUIList_withMaskAllTextDisabled_withMaskAllImagesEnabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIListFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIListRegions(regions: result, expectText: false) + } + + func testRedact_withSwiftUIList_withMaskAllTextEnabled_withMaskAllImagesDisabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIListFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIListRegions(regions: result, expectText: true) + } + + func testRedact_withSwiftUIList_withMaskAllTextDisabled_withMaskAllImagesDisabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIListFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIListRegions(regions: result, expectText: false) + } + + // MARK: Ignore background view + + func testCollectionViewListBackgroundDecorationView_isIgnoredSubtree_redactsAndDoesNotClipOut() throws { + // -- Arrange -- + // The SwiftUI List uses an internal decoration view + // `_UICollectionViewListLayoutSectionBackgroundColorDecorationView` which may have + // an extremely large frame. We ensure our builder treats this as a special case and + // redacts it directly instead of producing clip regions that could hide other masks. + guard let decorationView = try createCollectionViewListBackgroundDecorationView(frame: .zero) else { + throw XCTSkip("UICollectionView background decoration view is not available") + } + + // Configure a very large frame similar to what we see in production + decorationView.frame = CGRect(x: -20, y: -1_100, width: 440, height: 2_300) + decorationView.backgroundColor = .systemGroupedBackground + + // Add another redacted view that must remain redacted (no clip-out should hide it) + let titleLabel = UILabel(frame: CGRect(x: 16, y: 60, width: 120, height: 40)) + titleLabel.text = "Sample Text" + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 402, height: 874)) + rootView.addSubview(decorationView) + rootView.addSubview(titleLabel) + + // View Hierarchy: + // --------------- + // > + // | <_UICollectionViewListLayoutSectionBackgroundColorDecorationView: 0x119044de0; frame = (-20 -1100; 440 2300); backgroundColor = ; layer = > + // | > + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + let masked = createMaskedScreenshot(view: rootView, regions: result) + + // -- Assert -- + assertSnapshot(of: rootView, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + + // We should have at least two redact regions (label + decoration view) + XCTAssertGreaterThanOrEqual(result.count, 2) + // There must be no clipOut regions produced by the decoration view special-case + XCTAssertFalse(result.contains(where: { $0.type == .clipOut }), "No clipOut regions expected for decoration background view") + // Ensure we have at least one redact region that matches the large decoration view size + XCTAssertTrue(result.contains(where: { $0.type == .redact && $0.size == decorationView.bounds.size })) + } + + // - MARK: - SwiftUI.Image Redaction - SFSymbol + + private func setupSwiftUIImageFixture() throws -> UIWindow { + let view = VStack { + Image(systemName: "star.fill") + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 20, y: 20, width: 240, height: 320)) + + // View Hierarchy: + // --------------- + // == iOS 26 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackVS_5Image__: 0x107623670; frame = (0 0; 0 0); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | (layer) + // + // == iOS 18 & 17 & 16 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackVS_5Image__: 0x13dd2fe30; frame = (0 0; 240 320); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + } + + private func assertSwiftUIImageRegions(regions: [SentryRedactRegion]) throws { + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertCGSizeEqual(region.size, CGSize(width: 18.666, height: 18), accuracy: 0.01) + XCTAssertEqual(region.type, .redact) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 130.666, ty: 190.666), + accuracy: 0.01 + ) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } + + func testRedact_withSwiftUIImage_withMaskAllImagesEnabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIImageRegions(regions: result) + } + + func testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + XCTAssertEqual(result.count, 0) + } + + func testRedact_withSwiftUIImage_withMaskAllTextDisabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIImageRegions(regions: result) + } + + // - MARK: - SwiftUI.Image Redaction - UIImage + + private func setupSwiftUIImageWithUIImageFixture() throws -> UIWindow { + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + UIColor.green.setFill() + context.fill(CGRect(x: 0, y: 0, width: 20, height: 20)) + UIColor.purple.setFill() + context.fill(CGRect(x: 20, y: 0, width: 20, height: 20)) + UIColor.blue.setFill() + context.fill(CGRect(x: 0, y: 20, width: 20, height: 20)) + UIColor.orange.setFill() + context.fill(CGRect(x: 20, y: 20, width: 20, height: 20)) + } + + let view = VStack { + Image(uiImage: image) + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 20, y: 20, width: 240, height: 320)) + + // View Hierarchy: + // --------------- + // == iOS 26 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackVS_5Image__: 0x107623670; frame = (0 0; 0 0); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | (layer) + // + // == iOS 18 & 17 & 16 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackVS_5Image__: 0x13dd2fe30; frame = (0 0; 240 320); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + } + + private func assertSwiftUIImageWithUIImageRegions(regions: [SentryRedactRegion]) throws { + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertCGSizeEqual(region.size, CGSize(width: 18.666, height: 18), accuracy: 0.01) + XCTAssertEqual(region.type, .redact) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 130.666, ty: 190.666), + accuracy: 0.01 + ) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } + + func testRedact_withSwiftUIImage_withUIImage_withMaskAllImagesEnabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageWithUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIImageRegions(regions: result) + } + + func testRedact_withSwiftUIImage_withUIImage_withMaskAllImagesDisabled_shouldNotRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageWithUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: false) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + XCTAssertEqual(result.count, 0) + } + + func testRedact_withSwiftUIImage_withUIImage_withMaskAllTextDisabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIImageWithUIImageFixture() + + // -- Act -- + let sut = getSut(maskAllText: false, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIImageRegions(regions: result) + } + + // - MARK: - SwiftUI.Button Redaction + + private func setupSwiftUIButtonFixture() throws -> UIWindow { + let view = VStack { + Button(action: {}) { + Text("Tap Me") + } + } + return hostSwiftUIViewInWindow(view, frame: CGRect(x: 20, y: 20, width: 240, height: 320)) + + // View Hierarchy: + // --------------- + // == iOS 26 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_6ButtonVS_4Text___: 0x106821600; frame = (0 0; 240 320); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | <_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer: 0x600002c29700> (layer) + // + // == iOS 18 & 17 & 16 == + // ; layer = > + // | > + // | | > + // | | | <_TtGC7SwiftUI14_UIHostingViewGVS_6VStackGVS_6ButtonVS_4Text___: 0x103016a00; frame = (0 0; 240 320); autoresize = W+H; gestureRecognizers = ; backgroundColor = ; layer = > + // | | | | > + } + + private func assertSwiftUIButtonRegions(regions: [SentryRedactRegion]) throws { + let region = try XCTUnwrap(regions.element(at: 0)) + XCTAssertNil(region.color) + XCTAssertCGSizeEqual(region.size, CGSize(width: 18.666, height: 18), accuracy: 0.01) + XCTAssertEqual(region.type, .redact) + XCTAssertAffineTransformEqual( + region.transform, + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 112.333, ty: 190.666), + accuracy: 0.01 + ) + + // Assert that there are no other regions + XCTAssertEqual(regions.count, 1) + } + + func testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView() throws { + // -- Arrange -- + let window = try setupSwiftUIButtonFixture() + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: window) + let masked = createMaskedScreenshot(view: window, regions: result) + + // -- Assert -- + assertSnapshot(of: window, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 1)) + assertSnapshot(of: masked, as: .image, named: createTestDeviceOSBoundSnapshotName(index: 2)) + try assertSwiftUIButtonRegions(regions: result) + } + + // MARK: - Helper Methods + + /// Creates an instance of ``UIKit._UICollectionViewListLayoutSectionBackgroundColorDecorationView`` + /// + /// - Parameter frame: The frame to set for the created view + /// - Returns: The created view or `nil` if the type is absent + private func createCollectionViewListBackgroundDecorationView(frame: CGRect) throws -> UIView? { + return try createFakeView( + type: UIView.self, + name: "_UICollectionViewListLayoutSectionBackgroundColorDecorationView", + frame: frame + ) + } + + // MARK: - Layer Filtering Tests + + func testSwiftUIGraphicsView_withoutImageLayer_shouldNotRedact() throws { + // -- Arrange -- + // Create a fake SwiftUI._UIGraphicsView with a regular CALayer (not ImageLayer) + // This simulates SwiftUI using _UIGraphicsView for backgrounds + let graphicsView = try createFakeView( + type: UIView.self, + name: "SwiftUI._UIGraphicsView", + frame: CGRect(x: 20, y: 20, width: 40, height: 40) + ) + + guard let view = graphicsView else { + throw XCTSkip("SwiftUI._UIGraphicsView is not available") + } + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + rootView.addSubview(view) + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // Without ImageLayer, _UIGraphicsView should not be redacted + XCTAssertEqual(result.count, 0) + } + + func testSwiftUIGraphicsView_withImageLayer_shouldRedact() throws { + // -- Arrange -- + // Create a custom layer class to simulate SwiftUI.ImageLayer + class MockImageLayer: CALayer { + override class func description() -> String { + return "SwiftUI.ImageLayer" + } + } + + // Create a custom view that uses our mock image layer + class MockGraphicsView: UIView { + override class var layerClass: AnyClass { + return MockImageLayer.self + } + + override class func description() -> String { + return "SwiftUI._UIGraphicsView" + } + } + + let graphicsView = MockGraphicsView(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + rootView.addSubview(graphicsView) + + // -- Act -- + let sut = getSut(maskAllText: true, maskAllImages: true) + let result = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // With ImageLayer, _UIGraphicsView should be redacted + XCTAssertEqual(result.count, 1) + let region = try XCTUnwrap(result.first) + XCTAssertEqual(region.type, .redact) + XCTAssertEqual(region.size, CGSize(width: 40, height: 40)) + } +} + +#endif // os(iOS) diff --git a/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-1.png b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-1.png new file mode 100644 index 0000000000..5e316f7710 Binary files /dev/null and b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-1.png differ diff --git a/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-2.png b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-2.png new file mode 100644 index 0000000000..55c9a8e18d Binary files /dev/null and b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIButton_withMaskAllTextEnabled_withMaskAllImagesEnabled_shouldRedactView.iOS-17-5-2.png differ diff --git a/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-1.png b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-1.png new file mode 100644 index 0000000000..502c6e6ac1 Binary files /dev/null and b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-1.png differ diff --git a/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-2.png b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-2.png new file mode 100644 index 0000000000..e7501f5dfc Binary files /dev/null and b/Tests/SentryTests/ViewCapture/__Snapshots__/SentryUIRedactBuilderTests+SwiftUI/testRedact_withSwiftUIImage_withMaskAllImagesDisabled_shouldNotRedactView.iOS-17-5-2.png differ