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 parametersSent 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.
| Field | Meaning |
|---|---|
platform | ANDROID or IOS. Stated, so you don't infer it from other fields. |
type | The shared vocabulary — same word for the same thing on both platforms. See below. |
className | The raw platform type — android.widget.Button, XCUIElementTypeButton. Use it when you need the exact thing. |
selector | Ready to paste into a test, e.g. accessibility:test-LOGIN. |
selectorType | accessibility, id, or text — which strategy selector uses. |
accessibilityId | The accessibility id. content-desc on Android, the accessibility identifier on iOS. null if unset. |
resourceId | Android only. Always null on iOS — iOS has no equivalent. |
text | Visible text. text on Android; label, or value for a field showing its contents, on iOS. |
clickable | Can be tapped. On iOS this comes from the element type, since iOS exposes no such flag. |
isInput / isPassword | Something you type into; and whether it masks. |
bounds | [left,top][right,bottom]. iOS reports x/y/width/height and is converted to this. |
humanName | The best human-readable name for it, or null if it has none. |
nameQuality | GOOD, WEAK or MISSING — how reliably a test can refer to it. See Testability. |
namingSuggestion | What to change, and why. null when nothing needs doing. |
suggestedActions | TAP, 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:
type | Android | iOS |
|---|---|---|
Button | Button, ImageButton | XCUIElementTypeButton |
Input | EditText | TextField, SecureTextField, SearchField, TextView |
Text | TextView | XCUIElementTypeStaticText |
Image | ImageView | XCUIElementTypeImage |
Picker | Spinner | PickerWheel |
Slider | SeekBar | XCUIElementTypeSlider |
Switch / Checkbox / Radio | Switch, CheckBox, RadioButton | XCUIElementTypeSwitch |
List | RecyclerView, ListView | Table, CollectionView |
NavBar | Toolbar, ActionBar | NavigationBar |
TabBar | TabLayout | TabBar |
Alert | AlertDialog | Alert |
Container | ViewGroup, FrameLayout, LinearLayout, … | Other, Window |
Link | — (links live inside web views) | XCUIElementTypeLink |
Cell | — | XCUIElementTypeCell |
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
nameQuality | What it means | Will a test survive? |
|---|---|---|
GOOD | An accessibility identifier, or a meaningful resource id. | Yes — through redesigns and translation. |
WEAK | Findable 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. |
MISSING | Nothing to refer to at all. | No — only position or structure can find it, and both move. |
Bands
| Band | Score | |
|---|---|---|
READY | 80+ | Tests written against this app will hold up. |
NEEDS_WORK | 50–79 | Some controls need identifiers before automation is worth the effort. |
HARD_TO_AUTOMATE | below 50 | Most controls can only be found by position. Worth fixing before writing tests. |
NO_CONTROLS | — | Nothing 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.