nalexn / ViewInspector

Runtime introspection and unit testing of SwiftUI views
MIT License
2.18k stars 150 forks source link

When using in `UIViewRepresentable`, update the `Binding` value does not trigger `updateUIView(:)` during unit test #6

Closed dreampiggy closed 4 years ago

dreampiggy commented 4 years ago

Demo UIViewRepresentable Code: Just check https://github.com/SDWebImage/SDWebImageSwiftUI/blob/master/SDWebImageSwiftUI/Classes/AnimatedImage.swift :)

Demo Unit Test Code:

 func testAnimatedImageBinding() throws {
    let expectation = self.expectation(description: "AnimatedImage binding control")
    var binding = Binding<Bool>(wrappedValue: true)
    let imageView = AnimatedImage(name: "TestImage.gif", bundle: testImageBundle(), isAnimating: binding)
    let introspectView = imageView.introspectAnimatedImage { animatedImageView in
        if let animatedImage = animatedImageView.image as? SDAnimatedImage {
            XCTAssertEqual(animatedImage.animatedImageLoopCount, 0)
            XCTAssertEqual(animatedImage.animatedImageFrameCount, 5)
        } else {
            XCTFail("SDAnimatedImageView.image invalid")
        }
        XCTAssertTrue(animatedImageView.isAnimating)
        binding.wrappedValue = false
        binding.update()
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            XCTAssertFalse(binding.wrappedValue)
            XCTAssertFalse(imageView.isAnimating)
            XCTAssertFalse(animatedImageView.isAnimating) // <----- Failed on this line
            expectation.fulfill()
        }
    }
    _ = try introspectView.inspect(AnimatedImage.self)
    ViewHosting.host(view: introspectView)
    self.waitForExpectations(timeout: 5, handler: nil)
}
dreampiggy commented 4 years ago

This introspectAnimatedImage method is just a helper method using https://github.com/siteline/SwiftUI-Introspect to introspect the native UIView (UIImageView here for that animatedImageView variable). I want to test the case:

dreampiggy commented 4 years ago

The toturial you post here, seems only showing the case:

However, seems my case is:

nalexn commented 4 years ago

Hey @dreampiggy ,

Your use case should be possible to test, however, after you change the Binding value outside, you'd need to trigger the asynchronous inspection described in the next section. You may use inspect with delay parameter defaulting to zero.

dreampiggy commented 4 years ago

This issue can be closed.

For UIViewRepresentable, you have to wrap that into a standalone native SwiftUI view, using a @State to pass to the actual test view's @Binding. And it should have a closure that receive Self and send it outside. Because the structure is copied or you'll lost the @State status.

@nalexn Maybe this problem is about how SwiftUI handle the @Binding & @State ? I test that I can not use Binding(wrappedValue:) from ViewInspector, to mock the same behavior, must use @State, this is the root case.

Using the new test code solve the problem:

https://gist.github.com/dreampiggy/0b83d31428b3bed0e5be7ffd7ac4aa02

dreampiggy commented 4 years ago

Founded a new issue related to SwiftUI itself ?

Why same code, SwiftUI's NSViewRepresentable call updateNSView once, but UIViewRepresentable call updateUIView twice 😅

Not related to ViewInspector, but just a little curious. Which makes it's a pain to write cross-platform code and test code because of those behavior.