nalexn / ViewInspector

Runtime introspection and unit testing of SwiftUI views
MIT License
2.16k stars 148 forks source link

ViewInspector is unable to find Button in .toolbar modifier applied to UIViewControllerRepresentable #232

Open myost opened 1 year ago

myost commented 1 year ago

Recently I wrote a test in the large app that I work on for a view that used a UIViewControllerRepresentable with a toolbar modifier applied to it.

The test failed because it couldn't find the modifier even when searching the parent view for the accessibility identifier of the toolbar button. But, when I embedded the UIViewControllerRepresentable in a ZStack and applied the toolbar to the ZStack instead the test succeeded & the app still worked as expected. I wasn't trying to test other modifiers but it seemed like they were also duplicated in the output, so this could be an issue with any modifier applied to a UIViewControllerRepresentable. It seems like the UIViewControllerRepresentable is acting more like a Group than a View. I'm not sure if that is expected, but it would be great to be able to inspect a modifier that gets applied directly to the view instead of using the workaround.

Code Example

This is a min reproducible example of the view structure:

import SwiftUI

struct MyView: View {
    static let strings = ["Google", "Apple", "View Inspector"]

    @State var selected: String?
    @State var presentChild = false

    var body: some View {
        VStack {
            ForEach(Self.strings, id: \.self) { string in
                Button(action: {
                    selected = string
                    presentChild = true
                }, label: {
                    Text(string)
                        .foregroundColor(.blue)
                        .padding()
                })
            }
            if let selected {
                NavigationLink(
                    destination:
//                        ZStack {
                            ChildViewControllerRepresentable(title: selected)
//                        }
                        .navigationBarBackButtonHidden(true)
                        .toolbar {
                            ToolbarItem(placement: .navigationBarLeading) {
                                Button(action: {
                                    presentChild = false
                                    self.selected = nil
                                }, label: {
                                    Image(systemName: "chevron.left")
                                })
                                .accessibility(identifier: "ChildBackButton")
                            }
                        },
                    isActive: $presentChild,
                    label: {
                        EmptyView()
                    })
                .accessibility(identifier: "ParentNavigationLink")
            }
        }
    }
}

struct ChildViewControllerRepresentable: UIViewControllerRepresentable {
    let title: String

    func makeUIViewController(context: Context) -> WrapperViewController {
        return WrapperViewController(title: title)
    }

    class Coordinator { }

    func makeCoordinator() -> Self.Coordinator { Coordinator() }

    func updateUIViewController(_: WrapperViewController, context _: Context) { }
}

final class WrapperViewController: UIViewController {
    init(title: String?) {
        super.init(nibName: nil, bundle: nil)
        self.title = title
    }

    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

And the associated test method which will fail if you copy & paste the code from above, but will succeed if you comment the ZStack back in:

    func testNavLinkWhenSelectedObjectExists() throws {
        let testString = MyView.strings.first!
        let view = MyView(selected: testString, presentChild: true)

        let childView = try view
            .inspect()
            .find(viewWithAccessibilityIdentifier: "ParentNavigationLink")
            .find(ChildViewControllerRepresentable.self)
            .actualView()

        XCTAssertEqual(childView.title, testString)

        let _ = try view
            .inspect()
            .find(viewWithAccessibilityIdentifier: "ChildBackButton")
            .button()
            .tap()
    }

Here's a screenshot of the output when I run the test locally:

Screenshot 2023-03-09 at 5 43 55 PM