tandha — Centralized UI Automation-Identifier Toolkit for iOS¶
The original design brief for tandha. Kept for context; the shipped behavior is documented in the contract format and the README.
What this is¶
A Swift library + tooling that turns UI test automation identifiers into a single, shared source of truth. Define every automation ID once in a JSON file; assign it to UI elements via XIB, UIKit, or SwiftUI; and consume the exact same IDs from XCUITest and from external Appium-based tools (Katalon, Selenium-family).
One JSON contract, consumed by both sides: - Developers assign IDs to elements without inventing ad-hoc strings. - QA / automation engineers query the same IDs in their test suites.
No scattered magic strings, no silent drift when someone renames one.
The problem it solves¶
In large apps — especially banking/fintech with big regression suites — automation identifiers are typically: - hardcoded magic strings duplicated across the app code and the test suite, - inconsistently named across screens and teams, - silently broken when a developer renames one and QA's scripts still expect the old value, surfacing only as a red pipeline hours later.
There is no shared contract between the people who set IDs and the people who query them. This library makes the JSON that contract.
Important technical accuracy (read first — keep docs honest)¶
- The universal hook on iOS is
accessibilityIdentifier. Set it once and it is visible to: - XCUITest (native) via
element.identifier/ query subscripting, - Appium via the
accessibility idlocator strategy, - Katalon Studio (mobile) and Selenium-family tools through Appium — they do not drive native iOS directly; Appium's XCUITest driver is the bridge.
- Therefore the library's job is precisely: manage keys, reliably set
accessibilityIdentifier, and export the key list so any of those tools can consume the same contract. - Do NOT claim direct Selenium/Katalon native-iOS control anywhere in the README or marketing. Frame it as "sets the identifier every iOS automation stack relies on."
Scope (strict)¶
- In scope (v1): iOS/Swift — managing and assigning accessibility identifiers, XCUITest helpers, export for external tools, validation/lint.
- Out of scope (v1): the automation execution itself (this is NOT a test runner and not an Appium replacement), Android (possible v3 parity), web.
The JSON format (single source of truth — define this first)¶
Namespaced keys, grouped by screen/component. Addressed by dot path.
{
"login": {
"username_field": "login.username_field",
"password_field": "login.password_field",
"submit_button": "login.submit_button"
},
"dashboard": {
"balance_label": "dashboard.balance_label"
}
}
- Value rules: an explicit identifier string may be given; if omitted, the
identifier is auto-derived from the dot path (so
login.submit_button->"login.submit_button"). Explicit values win, for legacy IDs QA already uses. - Optional metadata per key (
description,screen,owner) for QA docs and generated page objects — ignored at runtime. - One file by default; support splitting into multiple files merged by namespace for large apps.
Type-safe access (codegen — headline feature)¶
An optional CLI / SwiftPM build-tool plugin generates a Swift enum/struct tree from the JSON so developers use compile-time-safe references with autocomplete instead of stringly-typed keys:
// generated
enum AutomationID {
enum login {
static let usernameField = "login.username_field"
static let submitButton = "login.submit_button"
}
}
Rename in JSON -> regenerate -> the compiler flags every stale reference. This is what eliminates drift on the developer side.
Assigning identifiers (three surfaces, mirror the same resolver)¶
- XIB / Storyboard:
@IBInspectable var automationKey: String?on aUIViewextension/subclass; onawakeFromNibit resolves the key and setsaccessibilityIdentifier. Lets QA-facing IDs be set right in Interface Builder. - UIKit (code):
view.setAutomationID(AutomationID.login.submitButton). - SwiftUI:
.automationID(AutomationID.login.submitButton)view modifier that wraps.accessibilityIdentifier(_:).
All three funnel through one resolver so behavior is identical regardless of how the element was built.
XCUITest support¶
Typed accessors so the test target reads the same source of truth the app ships with:
app.textFields[AutomationID.login.usernameField].tap()
app.buttons[AutomationID.login.submitButton].tap()
Provide a small XCUIApplication/XCUIElementQuery helper layer keyed by the
generated identifiers.
Interop / export (bridge to Appium / Katalon / Selenium-family)¶
A tandha export command emits the identifier map for external QA tooling so the
cross-platform suite consumes the same contract the app ships:
- flat JSON / CSV of key -> identifier,
- optional generated page-object stubs (e.g. an Appium/Katalon-friendly
class listing accessibility id locators).
This is the seam that lets a Katalon/Selenium+Appium suite stay in lockstep with the app without hand-copying strings.
Validation (CI-friendly)¶
tandha lint checks and returns a non-zero exit for CI:
- duplicate identifier values,
- unused keys (defined in JSON but never assigned in code),
- missing keys (referenced in tests/exports but absent from JSON),
- naming-convention enforcement (configurable regex, e.g. snake_case,
screen-prefixed).
Architecture (modular = contributor-friendly)¶
core parser -> flattener -> resolver (key -> identifier)
codegen JSON -> Swift enum tree (CLI + SwiftPM plugin)
bindings XIB (@IBInspectable) . UIKit helper . SwiftUI modifier
xcuitest typed query accessors for the test target
export flat JSON/CSV + page-object stub generators
lint duplicate / unused / missing / naming checks
- Each binding is a single-file contribution. Each export format is a
single-file contribution. Each lint rule is self-contained. These are
ideal
good first issuetickets.
Distribution¶
- Library: Swift Package Manager.
- Codegen: SwiftPM build-tool plugin (runs at build time) + standalone CLI.
- CLI: Homebrew formula later.
- Semantic versioning.
Phased roadmap¶
v1 — working core
- JSON spec + resolver
- Set accessibilityIdentifier via XIB / UIKit / SwiftUI
- XCUITest typed accessors
- Basic lint (duplicates / missing)
v2 — the differentiators - Codegen (type-safe enum tree) as CLI + SwiftPM plugin - Export for Appium/Katalon (JSON/CSV + page-object stubs) - Full lint (unused + naming conventions)
v3 — reach
- Android parity (Espresso/UIAutomator: resource-id / contentDescription)
- More export formats, richer metadata-driven QA docs
Contributor-friendliness¶
- Tight scope; bindings, export formats, and lint rules are all single-file PRs.
- Ship with:
CONTRIBUTING.md,good first issuelabels,hacktoberfesttopic, aREADMEwith a 60-second quickstart (define JSON -> assign in SwiftUI -> query in XCUITest). - If Android is added in v3, enforce an API-parity rule in
CONTRIBUTING.md.
Commit conventions¶
- Do NOT add any tool-attribution line (e.g. "Generated with …") to commit messages.
- Do NOT add a
Co-Authored-Bytrailer for tools/assistants. - Write commit messages as the human author only.
First batch of issues (starter backlog)¶
- Define and document the JSON format spec in
/docs/format.md. - Implement core: parser -> flattener -> resolver (key -> identifier).
- SwiftUI
.automationID()modifier wrapping.accessibilityIdentifier(). - UIKit
setAutomationID(_:)helper + XIB@IBInspectable automationKey. - XCUITest typed query accessors.
- Lint: duplicate identifiers + missing-key detection (CI exit codes).
- Codegen CLI: JSON -> Swift enum tree.
- SwiftPM build-tool plugin wrapping the codegen.
- Export command: flat JSON/CSV of key -> identifier.
READMEquickstart +CONTRIBUTING.md.
(v2/v3 issues — page-object export, unused/naming lint, Android parity — filed once v1 lands.)