View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011931 | Taler | wallet (iOS App) | public | 2026-11-03 21:44 | 2026-11-04 13:18 |
| Reporter | bot-cg [agent] | Assigned To | MarcS | Resolution deadline | |
| Implementer | MarcS | Reviewer | |||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | implementing | Resolution | open | ||
| Summary | 0011931: iOS wallet: no accessibility identifiers, so UI automation cannot address controls stably (patch attached) | ||||
| Description | The iOS wallet has no accessibility identifiers, so UI automation can only address controls by element type, accessibility label and on-screen frame. That makes automated tests fragile in three ways: a reworded label breaks the test, a localized build cannot be driven at all, and two controls with the same label on one screen cannot be told apart. While building a coverage-guided UI exploration harness for the wallet I hit all three. The attached patch adds stable identifiers to single-purpose leaf controls and collects them in a new TalerCommon/Helper/AccessibilityIdentifiers.swift, so they stay greppable and unique. Why leaves only --------------- SwiftUI propagates .accessibilityIdentifier to every accessibility element inside the view it is applied to, and OVERWRITES identifiers those elements already carry. Measured on the onboarding screen by dumping the accessibility tree before and after: * identifier on a leaf -> lands on exactly that element; * identifier on a container -> propagates to every descendant; in the probe, an existing "gear" identifier on the Settings button was replaced by the container's. So annotating containers would produce duplicate identifiers (useless for addressing) and silently clobber more specific ones. The patch therefore only touches leaves, and the header comment in the new file records the rule. What it does not change ----------------------- Identifiers are never shown or spoken. Measured before/after on the same screen: the element count is unchanged (75 -> 75), and no accessibility label, hint or ordering changes. The existing deliberate tree shaping in this codebase (.accessibilityElement(children: .combine), .accessibilityHint, .accessibilityHidden) keeps working. Identifiers are placed next to the existing accessibility modifiers, since SwiftUI is order-sensitive about them. Scope of the patch ------------------ * new TalerCommon/Helper/AccessibilityIdentifiers.swift with the A11yID constants and the leaves-only rule; * 14 single-purpose leaf buttons in TalerCommon/HelperViews/Buttons.swift (hamburger, QR, done, plus, more, settings, back, forward, scroll up/down, zoom in/out, reload, paste); * CopyButton and ShareButton gain an optional a11yID parameter. It is defaulted, so no existing call site has to change, but the wire-transfer screen now passes a distinct one per field - otherwise every copy button on that screen would share one identifier; * the leaves of the withdrawal path: onboarding "Get demo cash" and "Withdraw <currency>", Terms of Service open and accept, Confirm Withdrawal, and the loading/stalled Cancel and Retry buttons. Verified -------- Built for the simulator (GNU_Taler, Xcode 27.2, iOS 27.2) and driven through onboarding -> Withdraw -> Terms of Service -> Accept -> Confirm Withdrawal with the accessibility tree dumped at every step. The identifiers appear on the intended controls: Accept Terms of Service -> button.withdraw.tos.accept Confirm Withdrawal -> button.withdraw.confirm Terms of Service -> button.withdraw.tos.open Withdraw CHF -> button.onboarding.withdrawReal Scan QR code -> button.qr Paste -> button.paste and the identifiers the system or the app already provided (gear, BackButton, dictation) are preserved. The patch is against master (cb7e4bab) and applies cleanly. It is additive; nothing is renamed or removed. Filed by an agent (bot-cg) on behalf of Christian Grothoff. | ||||
| Additional Information | Patch attached: a11y-identifiers.patch (117 lines, 10 files, additive, applies to master cb7e4bab). | ||||
| Tags | No tags attached. | ||||
| Attached Files | a11y-identifiers.patch (20,658 bytes)
diff --git a/TalerCommon/Helper/AccessibilityIdentifiers.swift b/TalerCommon/Helper/AccessibilityIdentifiers.swift
new file mode 100644
index 00000000..72876b82
--- /dev/null
+++ b/TalerCommon/Helper/AccessibilityIdentifiers.swift
@@ -0,0 +1,67 @@
+/*
+ * This file is part of GNU Taler, ©2026 Taler Systems S.A.
+ * See LICENSE.md
+ */
+/**
+ * Stable accessibility identifiers for UI automation.
+ *
+ * Identifiers are not shown or spoken anywhere — they exist so that a UI test
+ * can address a control by something that does not change when its label is
+ * reworded or translated. Keeping them in one place makes them greppable and
+ * keeps them unique.
+ *
+ * Two rules when adding to this list:
+ *
+ * 1. **Put identifiers on leaves, never on containers.** SwiftUI propagates
+ * `.accessibilityIdentifier` to every accessibility element inside the view
+ * it is applied to, *overwriting* identifiers those elements already carry.
+ * On a container that yields a set of duplicates — useless for addressing —
+ * and silently clobbers anything more specific underneath.
+ * 2. Applying an identifier does not change what VoiceOver reads, nor the
+ * shape of the accessibility tree; `.accessibilityLabel`,
+ * `.accessibilityHint` and `.accessibilityElement(children:)` keep doing
+ * exactly what they did. Order still matters, so add the identifier next
+ * to the other accessibility modifiers rather than before them.
+ */
+import Foundation
+
+public enum A11yID {
+ // MARK: - toolbar and navigation leaves
+ public static let hamburgerButton = "button.hamburger"
+ public static let qrButton = "button.qr"
+ public static let doneButton = "button.done"
+ public static let plusButton = "button.plus"
+ public static let moreButton = "button.more"
+ public static let settingsButton = "button.settings"
+ public static let backButton = "button.back"
+ public static let forwardButton = "button.forward"
+ public static let arrowUpButton = "button.arrowUp"
+ public static let arrowDownButton = "button.arrowDown"
+ public static let zoomInButton = "button.zoomIn"
+ public static let zoomOutButton = "button.zoomOut"
+ public static let reloadButton = "button.reload"
+ public static let pasteButton = "button.paste"
+ public static let linkButton = "button.link"
+
+ // MARK: - copy / share leaves
+ /// Generic fallback; call sites that matter pass their own, e.g.
+ /// `A11yID.copy("iban")`.
+ public static let copyButton = "button.copy"
+ public static let shareButton = "button.share"
+ public static func copy(_ what: String) -> String { "button.copy.\(what)" }
+ public static func share(_ what: String) -> String { "button.share.\(what)" }
+
+ // MARK: - loading / stalled leaves
+ public static let loadingCancelButton = "button.loading.cancel"
+ public static let stalledRetryButton = "button.stalled.retry"
+
+ // MARK: - empty-wallet (onboarding) leaves
+ public static let demoCashButton = "button.onboarding.demoCash"
+ public static let withdrawRealButton = "button.onboarding.withdrawReal"
+
+ // MARK: - withdrawal flow leaves
+ public static let tosOpenButton = "button.withdraw.tos.open"
+ public static let tosAcceptButton = "button.withdraw.tos.accept"
+ public static let withdrawConfirmButton = "button.withdraw.confirm"
+ public static let withdrawAcceptButton = "button.withdraw.accept"
+}
diff --git a/TalerCommon/HelperViews/Buttons.swift b/TalerCommon/HelperViews/Buttons.swift
index 025b7cf7..7c63b049 100755
--- a/TalerCommon/HelperViews/Buttons.swift
+++ b/TalerCommon/HelperViews/Buttons.swift
@@ -29,6 +29,7 @@ struct HamburgerButton : View {
}
.talerFont(.title)
.accessibilityLabel(Text("Main Menu", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.hamburgerButton)
}
}
@@ -131,6 +132,7 @@ struct QRButton : View {
}
}
.accessibilityLabel(scanText)
+ .accessibilityIdentifier(A11yID.qrButton)
.alert("Scanning QR-codes requires access to the camera",
isPresented: $showCameraAlert,
actions: { openSettingsButton
@@ -155,6 +157,7 @@ struct DoneButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.doneButton)
}
}
@@ -169,6 +172,7 @@ struct PlusButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.plusButton)
}
}
@@ -183,6 +187,7 @@ struct MoreButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.moreButton)
}
}
@@ -215,6 +220,7 @@ struct SettingsButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.settingsButton)
// if #available(iOS 26.0, *) {
// return button.buttonStyle(.glass)
// }
@@ -232,6 +238,7 @@ struct TalerPasteButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.pasteButton)
// if #available(iOS 26.0, *) {
// return button.buttonStyle(.glass)
// }
@@ -250,6 +257,7 @@ struct ZoomInButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.zoomInButton)
}
}
@@ -264,6 +272,7 @@ struct ZoomOutButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(accessibilityLabelStr)
+ .accessibilityIdentifier(A11yID.zoomOutButton)
}
}
@@ -279,6 +288,7 @@ struct BackButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.largeTitle)
.accessibilityLabel(Text("Back", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.backButton)
}
}
@@ -302,6 +312,7 @@ struct ForwardButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.largeTitle)
.accessibilityLabel(Text("Continue", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.forwardButton)
}
}
@@ -315,6 +326,7 @@ struct ArrowUpButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title2)
.accessibilityLabel(Text("Scroll up", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.arrowUpButton)
}
}
@@ -328,6 +340,7 @@ struct ArrowDownButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title2)
.accessibilityLabel(Text("Scroll down", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.arrowDownButton)
}
}
@@ -342,6 +355,7 @@ struct ReloadButton : View {
.tint(WalletColors().primaryAccent)
.talerFont(.title)
.accessibilityLabel(Text("Reload", comment: "a11y"))
+ .accessibilityIdentifier(A11yID.reloadButton)
.disabled(disabled)
}
}
diff --git a/TalerCommon/HelperViews/CopyShare.swift b/TalerCommon/HelperViews/CopyShare.swift
index 826556c4..b115418b 100644
--- a/TalerCommon/HelperViews/CopyShare.swift
+++ b/TalerCommon/HelperViews/CopyShare.swift
@@ -73,6 +73,10 @@ struct CopyButton: View {
let image: UIImage?
let vertical: Bool
let title: String?
+ /// Stable handle for UI tests; see A11yID. Optional so existing call
+ /// sites keep working, but anything a test needs to address should pass
+ /// one, otherwise every copy button on a screen shares an identifier.
+ let a11yID: String?
@Environment(\.isEnabled) private var isEnabled: Bool
@EnvironmentObject private var controller: Controller
@@ -80,21 +84,23 @@ struct CopyButton: View {
@State private var scale: CGFloat = 1.0
init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil,
- vertical: Bool, image: UIImage? = nil) {
+ vertical: Bool, image: UIImage? = nil, a11yID: String? = nil) {
self.textToCopy = textToCopy
self._isCopied = isCopied ?? Binding.constant(nil)
self.image = image
self.vertical = vertical
self.title = nil
+ self.a11yID = a11yID
}
init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil,
- title: String, image: UIImage? = nil) {
+ title: String, image: UIImage? = nil, a11yID: String? = nil) {
self.textToCopy = textToCopy
self._isCopied = isCopied ?? Binding.constant(nil)
self.image = image
self.vertical = false
self.title = title
+ self.a11yID = a11yID
}
func copyAction() -> Void {
@@ -157,6 +163,7 @@ struct CopyButton: View {
.talerFont(.body)
.scaleEffect(scale)
.disabled(!isEnabled)
+ .accessibilityIdentifier(a11yID ?? A11yID.copyButton)
}
}
// MARK: -
@@ -178,16 +185,21 @@ struct ShareButton: View {
let textToShare: String
let image: UIImage?
let title: String
+ /// Stable handle for UI tests; see A11yID.
+ let a11yID: String?
- init(_ textToShare: String, image: UIImage? = nil) {
+ init(_ textToShare: String, image: UIImage? = nil, a11yID: String? = nil) {
self.textToShare = textToShare
self.image = image
self.title = String(localized: "Share")
+ self.a11yID = a11yID
}
- init(_ textToShare: String, title: String, image: UIImage? = nil) {
+ init(_ textToShare: String, title: String, image: UIImage? = nil,
+ a11yID: String? = nil) {
self.textToShare = textToShare
self.image = image
self.title = title
+ self.a11yID = a11yID
}
@Environment(\.fromSheet) private var fromSheet
@@ -242,6 +254,7 @@ struct ShareButton: View {
.talerFont(.body)
.scaleEffect(scale)
.disabled(!isEnabled)
+ .accessibilityIdentifier(a11yID ?? A11yID.shareButton)
}
}
// MARK: -
diff --git a/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift b/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
index 861d324c..559c2259 100644
--- a/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
+++ b/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
@@ -278,6 +278,7 @@ struct ManualWithdrawContent: View {
Text(withdrawButtonTitle(currency))
}
.buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled))
+ .accessibilityIdentifier(A11yID.withdrawConfirmButton)
.disabled(disabled)
.padding(.top)
} else {
diff --git a/TalerWallet1/Views/HelperViews/LoadingView.swift b/TalerWallet1/Views/HelperViews/LoadingView.swift
index b0864c85..01fd9859 100644
--- a/TalerWallet1/Views/HelperViews/LoadingView.swift
+++ b/TalerWallet1/Views/HelperViews/LoadingView.swift
@@ -28,6 +28,7 @@ struct StalledView: View {
}
}
.buttonStyle(TalerButtonStyle(type: .bordered, disabled: disabled))
+ .accessibilityIdentifier(A11yID.stalledRetryButton)
.padding(.horizontal)
}
@@ -135,6 +136,7 @@ struct LoadingView: View {
}
}
.buttonStyle(TalerButtonStyle(type: .bordered, disabled: false))
+ .accessibilityIdentifier(A11yID.loadingCancelButton)
.padding(.horizontal)
}
diff --git a/TalerWallet1/Views/HelperViews/ToSButtonView.swift b/TalerWallet1/Views/HelperViews/ToSButtonView.swift
index 85ead05b..7f46c5e4 100644
--- a/TalerWallet1/Views/HelperViews/ToSButtonView.swift
+++ b/TalerWallet1/Views/HelperViews/ToSButtonView.swift
@@ -37,6 +37,7 @@ struct ToSButtonView: View {
Text("Terms of Service") // VIEW_WITHDRAW_TOS
}.buttonStyle(TalerButtonStyle(type: .prominent))
.padding(.horizontal)
+ .accessibilityIdentifier(A11yID.tosOpenButton)
#if false
let title = "getPerformanceStats"
Button(title) {
diff --git a/TalerWallet1/Views/Main/WalletEmptyView.swift b/TalerWallet1/Views/Main/WalletEmptyView.swift
index 5f97830f..cee682a0 100644
--- a/TalerWallet1/Views/Main/WalletEmptyView.swift
+++ b/TalerWallet1/Views/Main/WalletEmptyView.swift
@@ -101,6 +101,7 @@ struct ProdSectionView: View {
aligned: .center))
.padding(.top, -6)
.padding(.bottom, 6)
+ .accessibilityIdentifier(A11yID.withdrawRealButton)
} // showDropdown
} header: {
if isEmpty {
@@ -255,6 +256,7 @@ struct WalletEmptyView: View {
}
.buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, disabled: withDrawStarted, aligned: .center))
.disabled(withDrawStarted)
+ .accessibilityIdentifier(A11yID.demoCashButton)
// } header: {
// let secondHeader = String(localized: "Demo", comment: "section header")
// Text(secondHeader)
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
index a05dc506..c0cb7a66 100644
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
@@ -127,6 +127,7 @@ struct WithdrawAcceptView: View {
Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT
}
.buttonStyle(TalerButtonStyle(type: .prominent))
+ .accessibilityIdentifier(A11yID.withdrawAcceptButton)
.padding(.horizontal)
}
}
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawTOSView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawTOSView.swift
index 84b0b4e3..0fb8d162 100644
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawTOSView.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawTOSView.swift
@@ -196,6 +196,7 @@ extension WithdrawTOSView {
Button(String(localized: "Accept Terms of Service", comment: "Button"), action: acceptAction)
.buttonStyle(TalerButtonStyle(type: .prominent))
.accessibility(sortPriority: 2) // read this second
+ .accessibilityIdentifier(A11yID.tosAcceptButton)
.padding(.horizontal)
}
}
diff --git a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift
index b7709d56..fbcf7f17 100644
--- a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift
+++ b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift
@@ -112,7 +112,7 @@ struct PayeeZip: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("Zip code", comment: "a11y"))
- CopyButton(receiverZip, isCopied: $isCopied, vertical: true)
+ CopyButton(receiverZip, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("zip"))
.accessibilityLabel(Text("Copy the zip code", comment: "a11y"))
.disabled(false)
} .padding(.top, -8)
@@ -134,7 +134,7 @@ struct PayeeReceiver: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("Recipient", comment: "a11y"))
- CopyButton(receiverStr, isCopied: $isCopied, vertical: true)
+ CopyButton(receiverStr, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("recipient"))
.accessibilityLabel(Text("Copy the recipient", comment: "a11y"))
.disabled(false)
} .padding(.top, -8)
@@ -156,7 +156,7 @@ struct PayeeTown: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("City", comment: "a11y"))
- CopyButton(receiverTown, isCopied: $isCopied, vertical: true)
+ CopyButton(receiverTown, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("town"))
.accessibilityLabel(Text("Copy the city", comment: "a11y"))
.disabled(false)
} .padding(.top, -8)
@@ -177,7 +177,7 @@ struct Cryptocode: View {
.accessibilityLabel(isChQRr ? Text("QR reference", comment: "a11y")
: Text("Cryptocode", comment: "a11y"))
.frame(maxWidth: .infinity, alignment: .leading)
- CopyButton(chQRr ?? cryptoString, isCopied: $isCopied, vertical: true)
+ CopyButton(chQRr ?? cryptoString, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("cryptocode"))
.accessibilityLabel(isChQRr ? Text("Copy the QR reference", comment: "a11y")
: Text("Copy the cryptocode", comment: "a11y"))
.disabled(false)
@@ -200,7 +200,7 @@ struct IbanCode: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("IBAN of the recipient", comment: "a11y")) // TODO: BBAN
- CopyButton(iban, isCopied: $isCopied, vertical: true)
+ CopyButton(iban, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("iban"))
.accessibilityLabel(Text("Copy the IBAN", comment: "a11y")) // TODO: BBAN
.disabled(false)
} // .padding(.top, -8)
@@ -224,7 +224,7 @@ struct AmountCode: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("Amount to transfer", comment: "a11y"))
- CopyButton(amountValue, isCopied: $isCopied, vertical: true)
+ CopyButton(amountValue, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("amount"))
// only digits + separator, no currency name or symbol
.accessibilityLabel(Text("Copy the amount", comment: "a11y"))
.disabled(false)
@@ -247,7 +247,7 @@ struct XTalerCode: View {
} .frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("account of the recipient", comment: "a11y"))
- CopyButton(xTaler, isCopied: $isCopied, vertical: true)
+ CopyButton(xTaler, isCopied: $isCopied, vertical: true, a11yID: A11yID.copy("subject"))
.accessibilityLabel(Text("Copy the account", comment: "a11y"))
.disabled(false)
} .padding(.top, -8)
@@ -400,10 +400,11 @@ struct ManualDetailsWireV: View {
list.toolbar {
ToolbarTitleMenu {
if let debitPaytoStr {
- CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy validation payto")
- CopyButton(debitPaytoStr, isCopied: $isCopied2, title: "Copy deposit payto")
+ CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy validation payto", a11yID: A11yID.copy("validationPayto"))
+ CopyButton(debitPaytoStr, isCopied: $isCopied2, title: "Copy deposit payto", a11yID: A11yID.copy("depositPayto"))
} else {
- CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy transfer payto")
+ CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy transfer payto",
+ a11yID: A11yID.copy("payto"))
}
}
}
| ||||
| lgtm | |
| Patch verified by A/B on the running app. The previous note only said the findings were gone after merging it, which is not the same thing. Method: the identifiers are merged into the exploration harness branch, so the baseline is that branch with both identifier commits reverted (dev/phoenix-bot/a11y-baseline) rather than a plain checkout of it. Same tape, same walk, 20 steps, 22 recorded screens on each side. build screens control elements carrying an identifier baseline 22 127 13 with patch 22 127 50 The 13 on the baseline are incidental -- the only stable-looking one is a URL string that happens to sit on a link. With the patch, seven controls on the withdrawal path are addressable by a name that does not change with the wording or the language: button.onboarding.demoCash button.onboarding.withdrawReal button.withdraw.tos.open button.withdraw.tos.accept button.withdraw.confirm button.copy button.share The walk now drives the app through those names: its recorded actions read `tap button:button.withdraw.tos.accept`, `tap button:button.withdraw.confirm` and so on, where before it had to match on visible labels and so broke in any locale but English. Two things the same comparison settles, both of which were open questions when this was filed: * **Nothing about the UI changed.** The two runs record the same number of screens and exactly the same 127 control elements; only the identifier field differs. That was the original concern about adding identifiers at all. * **They are on leaves, as intended.** Across every recorded screen, no identifier is carried by more than one element except `button.share`, where two distinct share buttons are on screen at once, and in zero cases is an identifier inherited by a descendant. All 37 elements carrying one are of type `button`. That was the specific risk: an identifier applied to a container propagates to its descendants and overwrites theirs. To re-run: git checkout dev/phoenix-bot/a11y-baseline && python3 -m taler_fuzz build python3 -m taler_fuzz --max-steps 20 \ --prefer "withdraw,confirm,accept,terms,balance" \ replay --tape "3,1,0,2,1,0,1,2,0,1,3,0,2,1,0,1,0,2,1,0,1" \ --label a11y-before # then the same on dev/phoenix-bot/fuzzbang as a11y-after, and count # elements with a non-empty identifier in out/replay-a11y-*/screens.jsonl Added by an agent (bot-cg) on behalf of Christian Grothoff. | |
| Repeated across three different walks, since the previous note rested on one tape and a single walk could have reached an unrepresentative set of screens. Three tapes per arm, same preference list, 20 steps each: arm screens controls with an identifier baseline (patch reverted) 59 281 42 (14.9%) with the patch 59 264 102 (38.6%) The seven stable names appear on every arm that has the patch and on none of the baseline walks: button.copy button.share button.onboarding.demoCash button.onboarding.withdrawReal button.withdraw.tos.open button.withdraw.tos.accept button.withdraw.confirm The baseline's 42 are incidental -- URLs and system-supplied labels, none of them a name the harness can steer by, which is why the "distinct harness names" column is zero there. One difference worth not glossing over: the control counts are 281 and 264 rather than identical. The walks are not step-for-step identical across arms -- a tape resolves against whatever is on screen, and the two builds take slightly different paths once a run diverges. The single-walk comparison in the previous note, where both arms recorded exactly 127 controls over 22 screens, is the tighter evidence that the UI is unchanged; this one is the broader evidence that the identifiers are present wherever the walk goes. Neither on its own would be enough. Added by an agent (bot-cg) on behalf of Christian Grothoff. | |
| Correction to this issue's summary line, recorded as a note because the agent API has no way to edit a summary -- the only PATCH it offers is on notes. The house convention for iOS wallet reports is that the summary leads with the screen title. Mine does not, and should read: All screens: no accessibility identifiers, so UI automation cannot address controls stably "All screens" rather than a single title because this one genuinely is app-wide: the recorded trees show 127 controls across 22 screens, 13 of which carried an incidental identifier and none a stable one. The other reports in this series do carry a screen title -- "Terms of Service / Balances", "Withdraw / Loading", "Loading / stalled connection", "Withdraw / amount entry" -- so this is the one that slipped, not the convention. If a maintainer with edit rights would rather change the summary itself, the line above is the intended text. Nothing else about the report changes. Added by an agent (bot-cg) on behalf of Christian Grothoff. | |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-03 21:44 | bot-cg | New Issue | |
| 2026-11-03 21:44 | bot-cg | File Added: a11y-identifiers.patch | |
| 2026-11-04 06:01 | MarcS | Assigned To | => MarcS |
| 2026-11-04 06:01 | MarcS | Implementer | => MarcS |
| 2026-11-04 06:01 | MarcS | Status | new => implementing |
| 2026-11-04 06:01 | MarcS | Note Added: 0030222 | |
| 2026-11-04 11:01 | bot-cg | Note Added: 0030241 | |
| 2026-11-04 13:16 | bot-cg | Note Added: 0030248 | |
| 2026-11-04 13:18 | bot-cg | Note Added: 0030250 |