Open rafael-assis opened 1 year ago
Hi @nalexn.
In an attempt to cover more scenarios in our usage of ViewInspector, @bachand and I hit this issue with @State
properties.
We'd really appreciate if we can have your input/perspective on it.
Thank you for your collaboration!
I’m experiencing a similar issue. In my case, I’m updating a @State
property after a button click and render its value in the same view. I would like to use ViewInspector
to assert the updated value in the View
instead of checking the @State
property directly. Per default this does not work and the value is always the initial value.
I’ve tried to make this work using Approach #2 and the proposal here to avoid having the extra boilerplate code for Inspectable
in my production code. But in this case I get the error Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
I’m experiencing a similar issue. In my case, I’m updating a
@State
property after a button click and render its value in the same view. I would like to useViewInspector
to assert the updated value in theView
instead of checking the@State
property directly. Per default this does not work and the value is always the initial value.I’ve tried to make this work using Approach #2 and the proposal here to avoid having the extra boilerplate code for
Inspectable
in my production code. But in this case I get the errorAccessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
@lo1tuma I tried a similar approach as the the one described in the link you shared with similar results: the View is not updated probably because the @State var numClicks: Int = 0
property's _location
is nil.
I even tried running the exact test in the example which fails probably because ContentView
is a nested/child view of TestWrapperView
.
import Combine
import XCTest
import SwiftUI
import ViewInspector
internal final class Inspection<V> {
let notice = PassthroughSubject<UInt, Never>()
var callbacks: [UInt: (V) -> Void] = [:]
func visit(_ view: V, _ line: UInt) {
if let callback = callbacks.removeValue(forKey: line) {
callback(view)
}
}
}
extension Inspection: InspectionEmissary {}
public let TEST_WRAPPED_ID: String = "wrapped"
struct TestWrapperView<Wrapped: View> : View{
internal let inspection = Inspection<Self>()
var wrapped: Wrapped
init( wrapped: Wrapped ){
self.wrapped = wrapped
}
var body: some View {
wrapped
.id(TEST_WRAPPED_ID)
.onReceive(inspection.notice) {
self.inspection.visit(self, $0)
}
}
}
struct ContentViewFromInternet: View {
@State var numClicks:Int = 0
var body: some View {
VStack{
Button("Click me"){
numClicks += 1
}.id("Button1")
Text("\(numClicks)")
.id("Text1")
.padding()
}
}
}
final class InternetTestCase: XCTestCase {
func testContentViewFromInternet() throws{
let sut = TestWrapperView(wrapped: ContentViewFromInternet())
let exp = sut.inspection.inspect { view in
let wrapped = try view.find(viewWithId: TEST_WRAPPED_ID)
let button = try wrapped.find(viewWithId: "Button1").button()
try button.tap()
let numClicks = try wrapped
.view(ContentViewFromInternet.self)
.actualView()
.numClicks
XCTAssertEqual(numClicks, 1)
let text = try wrapped.find(viewWithId: "Text1").text()
let value = try text.string()
XCTAssertEqual(value, "1")
}
ViewHosting.host(view: sut)
wait(for: [exp], timeout: 1)
}
}
Thanks for writing this up in such detail @rafael-assis . Ideally the "storage" for all @State
properties in view hierarchy would be functional during inspection. When only the outermost view's @State
properties are properly configured, it's easy to write a test that finds a subview, operates on that subview, and ultimately fails due to a @State
property of that subview not having any _location
.
Right, this is a known limitation of the library. Your investigation led in the right direction, ultimately, the _location = nil
is the reason why the state updates on the contained views don't work. You can read more about the mechanics of the inner state management in SwiftUI in this post.
The didAppear
workarounds in the guide are there exactly for this reason, so far I didn't find a better way to overcome the limitation
Thank you @nalexn for taking the time to review the issue and providing your thoughts! We'll be on the lookout for potential workarounds and keep this issue up to date.
@nalexn we're facing the same issue. Do you think it will technically be feasible on library-side? Or is it a SwiftUI limitation somehow? I didn't get that from the link you provided.
As @rafael-assis, we wanted to use a wrapper to prevent polluting our production code with inspection stuff. And it's working great for @ObservedObjects and stuff, but not at all for @States
Yeah, that's a limitation of SwiftUI. If you use objects for state management, they share the state and always contain the actual values, but @State does not
Any updates on this? I found also the same issue for @StateObject ... it's completely blocking me from using this library for testing..
Context
In an attempt to inspect SwiftUI Views that use
@State
Properties, I found out that these properties don't change their value, hence making it impossible to inspect a view after an action (a button tap for example) happened in order to validate its new state.I tried the Approach 1 listed in the guide section that covers workarounds to inspect
@State
properties.That approach seems to work only for the root level SwiftUI View. Other nested SwiftUI Views used in its
body
computed property woudn't have their state changed even though the find and tap functions will execute successfully throughout the child SwiftUI Views in the hierarchy.Code example
As a code example of the issue, consider the SwiftUI Views defined below:
Running a sample SwiftUI application, the following output was generated:
I noticed that
@State
properties that didn't work had their_location
property set to nil. This is a good indication of the cause of it not working.Another curious fact I found out about is that
@State
properties only had a non-nil_location
property in the context of their containing SwiftUI View'sbody
computed property or in the context of theonAppear
event handler which coincides with the statement in the aforementioned documentation:The inspection will be fully functional inside the didAppear callback.
Notice that in the output above the
_location
of the_childStatus
in the context of theParentView.onAppear
is nil.Test failure
The test below illustrates the issue as it fails with the following message:
testChildView(): XCTAssertNotEqual failed: ("childStatus: Initial") is equal to ("childStatus: Initial")