ViewInspector

repository·0.10.4·Indexed 25 days ago

https://github.com/nalexn/viewinspector

A library for unit testing SwiftUI views that enables runtime traversal of the view hierarchy. It provides tools to access View structs and their state, trigger user interactions and lifecycle events, and inspect standard and custom view properties, including gestures, button styles, label styles, and popups like Alerts, ActionSheets, and Sheets.

Tokens
14K
Snippets
25
Records
59
Agent score
83%

What's inside ViewInspector

  1. Verify Custom LabelStyle

    0.10.4

    To verify the label style applied to a view, use the .labelStyle() inspection method.

    To inspect the components provided by a LabelStyle configuration, use the following helpers on the view hierarchy:

    • styleConfigurationTitle(index: Int)
    • styleConfigurationIcon(index: Int)
    // Verify label style type
    XCTAssertTrue(try sut.inspect().labelStyle() is IconOnlyLabelStyle)
    
    // Inspect LabelStyle configuration components
    func testCustomLabelStyle() throws {
        let sut = CustomLabelStyle()
        let title = try sut.inspect().vStack().styleConfigurationTitle(0)
        let icon = try sut.inspect().vStack().styleConfigurationIcon(1)
        XCTAssertEqual(try title.blur().radius, 3)
        XCTAssertEqual(try icon.padding(), EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
    }
  2. Verify Custom ProgressViewStyle

    0.10.4

    To test a ProgressViewStyle with different completion states, use the inspect(fractionCompleted: Double?) helper function. This allows you to access the following configuration components:

    • styleConfigurationLabel(index: Int)
    • styleConfigurationCurrentValueLabel(index: Int)
    // Test ProgressViewStyle with specific fractionCompleted state
    func testCustomProgressViewStyle() throws {
        let sut = CustomProgressViewStyle()
        XCTAssertEqual(try sut.inspect(fractionCompleted: nil).vStack().styleConfigurationLabel(0).brightness(), 3)
        XCTAssertEqual(try sut.inspect(fractionCompleted: nil).vStack().styleConfigurationCurrentValueLabel(1).blur().radius, 5)
        XCTAssertEqual(try sut.inspect(fractionCompleted: 0.42).vStack().text(2).string(), "Completed: 42%")
    }
  3. Inspect UIKit views in UIViewRepresentable or UIViewControllerRepresentable

    0.10.4

    To access the underlying UIKit instance from a SwiftUI representable, use .actualView().uiView() or .actualView().viewController().

    Important: Because the UIKit hierarchy is added to the screen asynchronously, you must use an asynchronous inspection approach (like the Inspection class or the onAppear callback) to ensure the UIKit view is available before attempting to inspect it.

    let swiftuiView = try sut.inspect().find(MyCustomView.self)
    let uikitView = try swiftuiView.actualView().uiView() // or .viewController()
  4. Verify Custom ToggleStyle

    0.10.4

    To test a ToggleStyle with different isOn states, use the inspect(isOn: Bool) helper function to access the styleConfigurationLabel().

    // Test ToggleStyle with specific isOn state
    func testCustomToggleStyle() throws {
        let sut = CustomToggleStyle()
        XCTAssertEqual(try sut.inspect(isOn: false).styleConfigurationLabel().blur().radius, 0)
        XCTAssertEqual(try sut.inspect(isOn: true).styleConfigurationLabel().blur().radius, 5)
    }
  5. Inspect ActionSheet views

    0.10.4

    To inspect ActionSheet views, you must wrap them to make them inspectable.

    1. For isPresented: Binding<Bool> variant:

      • Add InspectableActionSheet to your main target.
      • Use actionSheet2 in your views.
      • Add extension InspectableActionSheet: PopupPresenter { } to your test target.
    2. For item: Binding<Item?> variant:

      • Add InspectableActionSheetWithItem to your main target.
      • Use actionSheet2 in your views.
      • Add extension InspectableActionSheetWithItem: ItemPopupPresenter { } to your test target.
  6. Use Async inspection with ViewHosting

    0.10.4

    When writing async tests, ViewHosting provides a closure-based API that automatically handles lifecycle management, removing the need for an explicit ViewHosting.expel() call.

    Usage: Wrap your inspection logic inside try await ViewHosting.host(view) { ... }.

    Handling Multiple Publishers: If your test involves multiple asynchronous events (like different publisher emissions), use a withThrowingDiscardingTaskGroup to ensure all inspection tasks subscribe to their respective publishers in time.

    // Basic Async Hosting
    let sut = TestView(flag: false)
    try await ViewHosting.host(sut) {
        try await sut.inspection.inspect { view in
            let text = try view.button().labelView().text().string()
            XCTAssertEqual(text, "false")
            sut.publisher.send(true)
        }
    }
    
    // Async with Task Groups for multiple publishers
    try await ViewHosting.host(sut) {
        try await withThrowingDiscardingTaskGroup { group in
            group.addTask {
                try await sut.inspection.inspect { view in
                    // ...
                }
            }
            group.addTask {
                try await sut.inspection.inspect(onReceive: sut.publisher) { view in
                    // ...
                }
            }
        }
    }
  7. Inspect Sheet views

    0.10.4

    To inspect Sheet views, you must wrap them to make them inspectable.

    1. For isPresented: Binding<Bool> variant:

      • Add InspectableSheet to your main target.
      • Use sheet2 in your views.
      • Add extension InspectableSheet: PopupPresenter { } to your test target.
    2. For item: Binding<Item?> variant:

      • Add InspectableSheetWithItem to your main target.
      • Use sheet2 in your views.
      • Add extension InspectableSheetWithItem: ItemPopupPresenter { } to your test target.
  8. Inspect FullScreenCover views

    0.10.4

    To inspect FullScreenCover views, you must wrap them to make them inspectable.

    1. For isPresented: Binding<Bool> variant:

      • Add InspectableFullScreenCover to your main target.
      • Use fullScreenCover2 in your views.
      • Add extension InspectableFullScreenCover: PopupPresenter { } to your test target.
    2. For item: Binding<Item?> variant:

      • Add InspectableFullScreenCoverWithItem to your main target.
      • Use fullScreenCover2 in your views.
      • Add extension InspectableFullScreenCoverWithItem: ItemPopupPresenter { } to your test target.
  9. Verify Custom ButtonStyle or PrimitiveButtonStyle

    0.10.4

    To verify which button style is applied to a view, use the .buttonStyle() inspection method.

    To test a ButtonStyle with different isPressed states, use the inspect(isPressed: Bool) helper function.

    For PrimitiveButtonStyle, it is recommended to inspect the internal custom view directly. You can use the PrimitiveButtonStyleConfiguration(onTrigger:) initializer to provide a closure that verifies when the trigger() method is called.

    // Verify button style type
    XCTAssertTrue(try sut.inspect().buttonStyle() is PlainButtonStyle)
    
    // Test ButtonStyle with specific isPressed state
    func testCustomButtonStyle() throws {
        let sut = CustomButtonStyle()
        XCTAssertEqual(try sut.inspect(isPressed: false).blur().radius, 0)
        XCTAssertEqual(try sut.inspect(isPressed: true).blur().radius, 5)
    }
    
    // Test PrimitiveButtonStyle trigger mechanism
    func testCustomPrimitiveButtonStyleButton() throws {
        let triggerExp = XCTestExpectation(description: "trigger()")
        triggerExp.expectedFulfillmentCount = 1
        let config = PrimitiveButtonStyleConfiguration(onTrigger: {
            triggerExp.fulfill()
        })
        let view = CustomPrimitiveButtonStyle.CustomButton(configuration: config)
        let exp = view.inspection.inspect { view in
            let label = try view.styleConfigurationLabel()
            try label.callOnTapGesture()
            // ... verify state changes
        }
        ViewHosting.host(view: view)
        wait(for: [exp, triggerExp], timeout: 0.1)
    }
  10. Inspect custom ViewModifiers

    0.10.4

    You can inspect a ViewModifier either as part of the view hierarchy or independently.

    Hierarchy Inspection: Use .modifier(Type.self) to extract the modifier from the view hierarchy, then use .viewModifierContent() to access the content placeholder view.

    Example:

    let modifier = try sut.inspect().emptyView().modifier(MyViewModifier.self)
    let content = try modifier.viewModifierContent()

    If the modifier uses @State or @Environment, use the asynchronous inspection approaches (Approach #1 or #2) described in the main guide.

    func testCustomViewModifierAppliedToHierarchy() throws {
        let sut = EmptyView().modifier(MyViewModifier())
        let modifier = try sut.inspect().emptyView().modifier(MyViewModifier.self)
        let content = try modifier.viewModifierContent()
        XCTAssertTrue(try content.hasPadding(.top))
        XCTAssertEqual(try content.padding(.top), 15)
    }
  11. Configure watchOS projects using @main

    0.10.4

    If your watchOS project uses the @main attribute, follow these steps to enable ViewInspector:

    1. Ensure you have a WKExtensionDelegate and reference it in your App struct using the @WKExtensionDelegateAdaptor property wrapper.
    2. Add let testViewSubject = TestViewSubject([]) as an instance variable to your ExtensionDelegate.
    3. Apply the .testable(extDelegate.testViewSubject) modifier to your root view (e.g., ContentView) inside the WindowGroup.

    Note: The implementation uses conditional compilation (#if !(os(watchOS) && DEBUG)) to ensure that TestViewSubject and the .testable() modifier are stripped out in Release builds.

    final class ExtensionDelegate: NSObject, WKExtensionDelegate {
        let testViewSubject = TestViewSubject([]) // #2
    }
    
    @main
    struct MyWatchOSApp: App {
        
        @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extDelegate // #1
        
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .testable(extDelegate.testViewSubject) // #3
            }
        }
    }
  12. Verify Custom GroupBoxStyle

    0.10.4

    To inspect the components provided by a GroupBoxStyle configuration, use these helpers:

    • styleConfigurationLabel(index: Int)
    • styleConfigurationContent(index: Int)
    // Inspect GroupBoxStyle configuration components
    func testCustomGroupBoxStyleInspection() throws {
        let sut = CustomGroupBoxStyle()
        XCTAssertEqual(try sut.inspect().vStack().styleConfigurationLabel(0).brightness(), 3)
        XCTAssertEqual(try sut.inspect().vStack().styleConfigurationContent(1).blur().radius, 5)
    }