Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions devices/devicekit/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ func isVisible(rect sourceTreeElementRect) bool {
return rect.X >= 0 && rect.Y >= 0 && rect.Width > 0 && rect.Height > 0
}

// hasText reports whether an optional string attribute carries a value; an
// empty string identifies an element no better than a missing one.
func hasText(value *string) bool {
return value != nil && *value != ""
}

// filterSourceElements converts a WDA source tree into ScreenElements,
// preserving hierarchy: filtered descendants of an accepted element become its
// Children, while descendants of rejected elements are hoisted to the nearest
Expand All @@ -45,7 +51,14 @@ func filterSourceElements(source sourceTreeElement) []types.ScreenElement {
childElements = append(childElements, filterSourceElements(child)...)
}

acceptedTypes := []string{"TextField", "TextView", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image", "SecureTextField", "WebView"}
acceptedTypes := []string{
"TextField", "TextView", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image", "SecureTextField", "WebView",
// types that clients map to semantic roles (slider, progressbar,
// combobox, link, tab, header, alert, list, listitem)
"Slider", "ProgressIndicator", "ActivityIndicator", "Picker", "PickerWheel",
"Link", "Tab", "TabBar", "NavigationBar", "Toolbar", "Alert", "Sheet",
"Cell", "Table", "CollectionView", "ScrollView",
}

// strip XCUIElementType prefix if present
elementType := strings.TrimPrefix(source.Type, "XCUIElementType")
Expand All @@ -60,16 +73,16 @@ func filterSourceElements(source sourceTreeElement) []types.ScreenElement {

// elements explicitly tagged with accessibilityIdentifier are always
// included, regardless of type, see https://github.com/mobile-next/mobilecli/issues/341
if source.RawIdentifier != nil && *source.RawIdentifier != "" {
if hasText(source.RawIdentifier) {
typeAccepted = true
}

if !typeAccepted || !isVisible(source.Rect) {
return childElements
}

hasIdentifier := source.Label != nil || source.Name != nil || source.RawIdentifier != nil || source.PlaceholderValue != nil
alwaysInclude := elementType == "TextField" || elementType == "TextView" || elementType == "SecureTextField" || elementType == "Button" || elementType == "Switch" || elementType == "SearchField" || elementType == "WebView"
hasIdentifier := hasText(source.Label) || hasText(source.Name) || hasText(source.RawIdentifier) || hasText(source.PlaceholderValue)
alwaysInclude := elementType == "TextField" || elementType == "TextView" || elementType == "SecureTextField" || elementType == "Button" || elementType == "Switch" || elementType == "SearchField" || elementType == "WebView" || elementType == "Slider" || elementType == "Picker" || elementType == "PickerWheel"
if !hasIdentifier && !alwaysInclude {
return childElements
}
Expand Down
81 changes: 81 additions & 0 deletions devices/devicekit/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,84 @@ func TestFilterSourceElementsOmitsChildrenFromJsonWhenEmpty(t *testing.T) {
t.Errorf("expected leaf element to have nil Children, got %+v", output[0].Children)
}
}

func TestFilterSourceElementsIncludesTypesBehindSemanticRoles(t *testing.T) {
// Clients map these types to semantic roles (slider, progressbar, alert,
// combobox, list, listitem, tab, link, header). A labeled element of any
// of them must survive filtering, or the role can never match on iOS.
roleTypes := []string{
"Slider", "ProgressIndicator", "ActivityIndicator", "Picker", "PickerWheel",
"Link", "Tab", "TabBar", "NavigationBar", "Toolbar", "Alert", "Sheet",
"Cell", "Table", "CollectionView", "ScrollView",
}

for _, roleType := range roleTypes {
output := filterSourceElements(sourceTreeElement{
Type: "XCUIElementType" + roleType,
Label: strPtr("labeled"),
Rect: visibleRect(0, 100, 300, 44),
})

if len(output) != 1 || output[0].Type != roleType {
t.Errorf("expected a labeled %s to be included, got %+v", roleType, output)
}
}
}

func TestFilterSourceElementsIncludesUnlabeledAdjustableControls(t *testing.T) {
// Sliders and pickers are interactive yet often carry no label, so they
// are always included, like TextField and Switch.
for _, controlType := range []string{"Slider", "Picker", "PickerWheel"} {
output := filterSourceElements(sourceTreeElement{
Type: "XCUIElementType" + controlType,
Rect: visibleRect(0, 100, 300, 44),
})

if len(output) != 1 || output[0].Type != controlType {
t.Errorf("expected an unlabeled %s to be included, got %+v", controlType, output)
}
}
}

func TestFilterSourceElementsStillDropsUnlabeledContainers(t *testing.T) {
// Unlabeled containers stay out of the dump to keep it small; their
// children are hoisted as before.
output := filterSourceElements(sourceTreeElement{
Type: "XCUIElementTypeTable",
Rect: visibleRect(0, 0, 402, 874),
Children: []sourceTreeElement{
{
Type: "XCUIElementTypeButton",
Label: strPtr("Join"),
Rect: visibleRect(338, 194, 31, 20),
},
},
})

if len(output) != 1 || output[0].Type != "Button" {
t.Fatalf("expected only the hoisted Button, got %+v", output)
}
}

func TestFilterSourceElementsTreatsEmptyIdentifiersAsAbsent(t *testing.T) {
// A container whose label/name/placeholder are present but empty is as
// anonymous as one without them: drop it and hoist its children.
output := filterSourceElements(sourceTreeElement{
Type: "XCUIElementTypeTable",
Label: strPtr(""),
Name: strPtr(""),
PlaceholderValue: strPtr(""),
Rect: visibleRect(0, 0, 402, 874),
Children: []sourceTreeElement{
{
Type: "XCUIElementTypeButton",
Label: strPtr("Join"),
Rect: visibleRect(338, 194, 31, 20),
},
},
})

if len(output) != 1 || output[0].Type != "Button" {
t.Fatalf("expected only the hoisted Button, got %+v", output)
}
}
Loading