Skip to main content

What a crawl gives you back

A crawl walks the app, reads each screen, and returns the elements it found. Android and iOS return the same shape — a test-authoring tool written against one works unchanged against the other.

Run one with:

POST /api/mobile-apps/{id}/inspect?crawlMode=true&maxDepth=3
crawlMode and maxDepth are query parameters

Sent in the JSON body they are silently ignored and you get a single-screen inspection instead. The response echoes the values actually used — check crawlMode in it if you get one screen when you expected several.

Read the results with GET /api/mobile-apps/{id}/elements.

One element

{
"platform": "IOS",
"index": 3,
"type": "Button",
"className": "XCUIElementTypeButton",
"selector": "accessibility:Text Button",
"selectorType": "accessibility",
"accessibilityId": "Text Button",
"resourceId": null,
"text": "Text",
"clickable": true,
"focusable": false,
"isInput": false,
"isPassword": false,
"enabled": true,
"bounds": "[0,73][375,133]",
"depth": 12,
"humanName": "Text Button",
"nameQuality": "GOOD",
"namingSuggestion": null,
"suggestedActions": ["TAP"]
}

Every key is always present, on both platforms. Where a platform has no equivalent the value is null or false — never a missing key, so you never have to check the platform before reading a field.

FieldMeaning
platformANDROID or IOS. Stated, so you don't infer it from other fields.
typeThe shared vocabulary — same word for the same thing on both platforms. See below.
classNameThe raw platform type — android.widget.Button, XCUIElementTypeButton. Use it when you need the exact thing.
selectorReady to paste into a test, e.g. accessibility:test-LOGIN.
selectorTypeaccessibility, id, or text — which strategy selector uses.
accessibilityIdThe accessibility id. content-desc on Android, the accessibility identifier on iOS. null if unset.
resourceIdAndroid only. Always null on iOS — iOS has no equivalent.
textVisible text. text on Android; label, or value for a field showing its contents, on iOS.
clickableCan be tapped. On iOS this comes from the element type, since iOS exposes no such flag.
isInput / isPasswordSomething you type into; and whether it masks.
bounds[left,top][right,bottom]. iOS reports x/y/width/height and is converted to this.
humanNameThe best human-readable name for it, or null if it has none.
nameQualityGOOD, WEAK or MISSING — how reliably a test can refer to it. See Testability.
namingSuggestionWhat to change, and why. null when nothing needs doing.
suggestedActionsTAP, SEND_KEYS — what this element is for.

The shared type vocabulary

A button is a button. Where the two platforms mean the same thing, they get the same word:

typeAndroidiOS
ButtonButton, ImageButtonXCUIElementTypeButton
InputEditTextTextField, SecureTextField, SearchField, TextView
TextTextViewXCUIElementTypeStaticText
ImageImageViewXCUIElementTypeImage
PickerSpinnerPickerWheel
SliderSeekBarXCUIElementTypeSlider
Switch / Checkbox / RadioSwitch, CheckBox, RadioButtonXCUIElementTypeSwitch
ListRecyclerView, ListViewTable, CollectionView
NavBarToolbar, ActionBarNavigationBar
TabBarTabLayoutTabBar
AlertAlertDialogAlert
ContainerViewGroup, FrameLayout, LinearLayout, …Other, Window
Link— (links live inside web views)XCUIElementTypeLink
CellXCUIElementTypeCell

Where the platforms genuinely differ, they keep different words. Forcing a shared name onto two different concepts is worse than having two, because it makes you confident about something untrue.

The live example is TextView. On Android it's a read-only label; on iOS it's a multi-line box you type into. So Android's becomes Text and iOS's becomes Input, and they deliberately do not meet — a shared name would tell you that you can send keys to an Android label.

When the mapper doesn't know something

An unrecognised type keeps its own name rather than being swept into Container. An honest unknown is more useful than a confident wrong answer, and className always carries the exact platform type, so mapping never loses information.

A mapper over someone else's UI toolkit is never finished. A developer can name a custom control anything, and both platforms keep adding types. So rather than pretending the table is complete, the crawl summary counts what it didn't recognise:

"unmappedTypes": { "FancyDial": 4, "CustomChart": 1 }

Empty is the healthy case. Anything listed is a control your users have that we can only describe by its raw platform name — those elements still come back with selectors and are still usable, they just don't carry a shared type. Send us the names and they get mapped; that list is how the vocabulary gets maintained from what real apps contain, rather than from someone remembering to revisit it.

The crawl summary

{
"totalElements": 104,
"uniqueSelectors": 32,
"clickableElements": 32,
"elementsWithSelectors": 104,
"inputElements": 1,
"totalScreens": 6,
"typeBreakdown": { "Button": 29, "Text": 35, "Image": 8, "…": 0 }
}

Read uniqueSelectors, not totalElements. A multi-screen crawl re-finds the navigation bar and tab bar on every screen, so the raw total counts the same control several times. In the example above, 104 elements are 32 distinct things. The raw count is still there because per-screen occurrences matter when you care where something appeared.

Testability

A crawl can see which controls have nothing stable to grab hold of. That's the most useful thing it learns, and the only part your team can fix — so it comes back as advice rather than being left for you to infer from the element list.

"testabilityAdvisory": {
"score": 72,
"band": "NEEDS_WORK",
"controlsAssessed": 27,
"wellNamed": 19,
"needsAttention": 8,
"summary": "19 of 27 controls have a stable identifier. 8 can only be found by text or position, so tests touching them will break when the layout or wording changes.",
"examples": [
{
"type": "Button",
"nameQuality": "MISSING",
"text": null,
"selector": null,
"suggestion": "This button has no accessibility identifier and no visible text, so a test can only find it by its position on screen — which breaks the next time the layout changes. Give it an accessibility identifier."
}
]
}

The three grades

nameQualityWhat it meansWill a test survive?
GOODAn accessibility identifier, or a meaningful resource id.Yes — through redesigns and translation.
WEAKFindable only by visible text, or by an id that looks auto-generated (view123, a hex blob).Until someone edits the copy, or you ship another language.
MISSINGNothing to refer to at all.No — only position or structure can find it, and both move.

Bands

BandScore
READY80+Tests written against this app will hold up.
NEEDS_WORK50–79Some controls need identifiers before automation is worth the effort.
HARD_TO_AUTOMATEbelow 50Most controls can only be found by position. Worth fixing before writing tests.
NO_CONTROLSNothing interactive was found on the screens crawled. Not a score of zero.

What is not counted against you

Only interactive controls are assessed — buttons, inputs, anything you tap or type into. Your app isn't less testable because its body text has no accessibility identifier, and reporting that would bury the findings that matter.

A good name is never flagged. If nameQuality is GOOD, namingSuggestion is null. An advisory that nags about things which are already fine is one people stop reading.

Fixing it

Add an accessibility identifier to the control. On Android that's contentDescription or a resource-id; on iOS it's accessibilityIdentifier. Both are invisible to users, both survive redesigns and translations, and both make the element addressable by name instead of by position.

That work pays off twice — the same identifiers make your app usable with a screen reader.

What is deliberately left out

The app root. iOS exposes the whole application as an element with a name. It passed the filter and produced one dead row per screen; nobody taps "the app".

Anything with no way to address it and nothing to read. An element with no accessibility id, no resource id and no text can't be turned into a selector or an assertion, so it isn't returned.

Known gaps

Tappability on iOS is decided by element type, from a deliberately narrow list. A custom control — particularly something SwiftUI-built reporting an unusual type — may come back clickable: false. If that happens, the className is the thing to report.

Nested iOS navigation is lightly exercised. Crawls reliably reach screens one tap from the start; deeper flows have not been proven on a real iOS app.