5

I have a VStack with code relying on the .onTapGesture method. Something like this:

VStack {
    if imageShow {
        Image("image1")
    }
    else {
        Image("image2")
    }
}
.onTapGesture {
    imageShow.toggle()
}

I'd like to test this behavior within a UI Test using XCTest. The problem is, I don't know how to access the VStack in order to apply a .tap() to it. I can't seem to find the method attached to app. A button is found using app.buttons[] but there doesn't seem to be an equivalent for app.VStack or app.HStack.

Also, I've tried converting this code to wrap the VStack in a Button, but for some reason, this overlays my image, distorting the preferred behavior.

Updating with full VStack code snippet that I am working with:

VStack(alignment: .leading, spacing: 0) {
    ZStack {
        if self.create_event_vm.everyone_toggle == true {
            Image("loginBackground")
                .resizable()
                .accessibility(identifier: "everyone_toggle_background")
            }
        HStack {
            VStack(alignment: .leading, spacing: 0) {
                Text("Visible to everyone on Hithr")
                    .kerning(0.8)
                    .scaledFont(name: "Gotham Medium", size: 18)                                          .foregroundColor(self.create_event_vm.everyone_toggle ? Color.white : Color.black)
                    .frame(alignment: .leading)

                    Text("Public event")
                        .kerning(0.5)
                        .scaledFont(name: "Gotham Light", size: 16)
                                    .foregroundColor(self.create_event_vm.everyone_toggle ? Color.white : Color.black)
                        .frame(alignment: .leading)
                    }
                    .padding(.leading)
                    Spacer()
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: 74)
        .onTapGesture {
            self.create_event_vm.everyone_toggle.toggle()
        }
        .accessibility(identifier: "public_event_toggle")
        .accessibility(addTraits: .isButton)
Bonteq
  • 777
  • 7
  • 24

3 Answers3

5

Try the following approach

VStack {
    if imageShow {
        Image("image1")
    }
    else {
        Image("image2")
    }
}
.onTapGesture {
    imageShow.toggle()
}
.accessibility(addTraits: .isButton)
.accessibility(identifier: "customButton")

and test

XCTAssertTrue(app.buttons["customButton"].exists)
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 3
    It's giving me the following error: `Failed to get matching snapshot: Multiple matching elements found for`. It looks like it's attaching itself to every `Text()` view within the VStack. – Bonteq Jun 15 '20 at 16:58
  • 1
    You can use app.buttons["something"].firstMatch to get around the Multiple matching elements found error. – Howard Shere Apr 26 '21 at 18:46
  • There is a better way, see my answer :) – Alexander M. Sep 15 '22 at 21:19
5

The solution for me was to create an un-styled GroupBox to "wrap" any content inside.
After that we can assign an accessibilityIdentifier to it.
It doesn't disrupt the screen readers or change the layout.

Code:

/// Groupbox "no-op" container style without label
/// to combine the elements for accessibilityId
struct ContainerGroupBoxStyle: GroupBoxStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.content
    }
}

extension GroupBoxStyle where Self == ContainerGroupBoxStyle {
    static var contain: Self { Self() }
}

extension View {
    /// This method wraps the view inside a GroupBox
    /// and adds accessibilityIdentifier to it
    func groupBoxAccessibilityIdentifier(_ identifier: String) -> some View {
        GroupBox {
            self
        }
        .groupBoxStyle(.contain)
        .accessibilityIdentifier(identifier)
    }
}

Usage:

struct TestView: View {
    var body: some View {
        HStack {
            Image(systemName: "person")
            TextField("Name", text: .constant("Name"))
        }
        .padding()
        .onTapGesture {
            print("Stack Tap")
        }
        .groupBoxAccessibilityIdentifier("NAME_TEXTFIELD")
    }
}

Locating in XCTest:

app.descendants(matching: .any)["NAME_TEXTFIELD"]
// OR
app.otherElements["NAME_TEXTFIELD"]

Note: GroupBox is unavailable in tvOS and watchOS.

Alexander M.
  • 3,308
  • 2
  • 20
  • 31
  • 1
    how do you access it from a UI test? with `groups`, `otherElements` or something else? also note: group box is unavailable in tvOS and watchOS – freya Mar 25 '23 at 15:20
  • 1
    @freya Ty for your input, I've updated my answer. You can use `.any` match or `otherElements`, yup. I've checked that it works in Accessibility Inspector + verified with my Appium colleague that he is happy with the results. Never tried it in `XCTest` :) – Alexander M. Mar 29 '23 at 21:39
1

I find that otherElements["x"] doesn't work for ZStack, VStack or HStack. Instead I identify something else, like a Text, and try to get using staticTexts["x"].

Everton Cunha
  • 1,017
  • 8
  • 10