commit 445759b337b14688b25b2615694b3b7a707a95ea
parent cb7e4bab131dee8ee0cb9098d9e82484d502a8f4
Author: Marc Stibane <marc@taler.net>
Date: Wed, 4 Nov 2026 06:45:20 +0100
AI: accessibilityIdentifier
Diffstat:
10 files changed, 113 insertions(+), 14 deletions(-)
diff --git a/TalerCommon/Helper/AccessibilityIdentifiers.swift 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
@@ -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
@@ -73,6 +73,7 @@ struct CopyButton: View {
let image: UIImage?
let vertical: Bool
let title: String?
+ let a11yID: String? // unique ID for UI-testing
@Environment(\.isEnabled) private var isEnabled: Bool
@EnvironmentObject private var controller: Controller
@@ -80,21 +81,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 +160,7 @@ struct CopyButton: View {
.talerFont(.body)
.scaleEffect(scale)
.disabled(!isEnabled)
+ .accessibilityIdentifier(a11yID ?? A11yID.copyButton)
}
}
// MARK: -
@@ -178,16 +182,20 @@ struct ShareButton: View {
let textToShare: String
let image: UIImage?
let title: String
+ 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 +250,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"))
}
}
}