Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
03af8e2
test(swift-sdk): add wallet persistence UI tests + extract test helpers
llbartekll Apr 28, 2026
b3c85a2
test(swift-sdk): add testnet identity-discovery UI test
llbartekll Apr 28, 2026
7c0ae94
fix(swift-sdk): address PR #3560 review findings
llbartekll Apr 29, 2026
c08f9fa
fix(swift-sdk): address PR #3560 follow-up review
llbartekll Apr 29, 2026
52312a2
ci(swift-sdk): wire UI_TEST_MNEMONIC secret into smoke workflow
llbartekll Apr 29, 2026
e8eb123
ci(swift-sdk): run Swift Example App UI smoke nightly
llbartekll Apr 29, 2026
4e4cf1a
docs(swift-sdk): add UI test suite README
llbartekll Apr 29, 2026
296d966
fix(swift-sdk): address PR #3560 follow-up review (4e4cf1ad)
llbartekll Apr 29, 2026
3823ee4
Merge branch 'v3.1-dev' into claude/swift-xcuitest-wallet-persistence
QuantumExplorer May 1, 2026
33b6f3c
fix(swift-sdk): address PR #3560 follow-up review (3823ee4a)
llbartekll May 4, 2026
a228c04
Merge remote-tracking branch 'origin/v3.1-dev' into claude/swift-xcui…
llbartekll May 12, 2026
983f89c
fix(swift-sdk): address PR #3560 follow-up review
llbartekll May 12, 2026
4801f03
fix(swift-sdk): address PR #3560 follow-up review
llbartekll May 12, 2026
0e38b24
fix(swift-sdk): scrub stale Search Wallets selectors from runIdentity…
llbartekll May 12, 2026
92cddcd
savepoint
llbartekll May 12, 2026
8cfad23
fix(swift-sdk): tighten UI test helpers and network-status label
llbartekll May 18, 2026
be5f3fa
fix(swift-sdk): preserve identity balance sign through accessibility …
llbartekll May 18, 2026
a8ada7b
Merge remote-tracking branch 'origin/v3.1-dev' into claude/swift-xcui…
llbartekll May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/swift-example-app-ui-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ jobs:
-scheme SwiftExampleApp \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-only-testing:SwiftExampleAppUITests/SwiftExampleAppUITests/testCreateGeneratedWalletFlow \
-only-testing:SwiftExampleAppUITests/WalletPersistenceTests/testWalletPersistsAcrossRelaunch \
-only-testing:SwiftExampleAppUITests/WalletPersistenceTests/testWalletDeletionCleanupSurvivesRelaunch \
-parallel-testing-enabled NO \
-maximum-concurrent-test-simulator-destinations 1 \
-resultBundlePath "$RESULT_BUNDLE_PATH"
Comment thread
llbartekll marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ struct ContentView: View {

// Tab 3: Identities
IdentitiesTabView()
.accessibilityIdentifier("rootTab.identities")
.tabItem {
Label("Identities", systemImage: "person.crop.circle")
}
Expand All @@ -107,6 +108,7 @@ struct ContentView: View {

// Tab 5: Settings (includes Platform section)
SettingsView()
.accessibilityIdentifier("rootTab.settings")
.tabItem {
Label("Settings", systemImage: "gearshape")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ struct CreateWalletView: View {

Section {
Toggle("Import Existing Wallet", isOn: $showImportOption)
.accessibilityIdentifier("createWallet.importToggle")
} header: {
Text("Options")
}
Expand Down Expand Up @@ -164,6 +165,7 @@ struct CreateWalletView: View {
.autocorrectionDisabled()
.lineLimit(3...6)
.focused($focusedField, equals: .mnemonic)
.accessibilityIdentifier("createWallet.mnemonicField")
} header: {
Text("Recovery Phrase")
} footer: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ struct IdentitiesContentView: View {
} label: {
Label("Search Wallets for Identities", systemImage: "magnifyingglass")
}
.accessibilityIdentifier("identities.searchWalletsMenuItem")
} label: {
Image(systemName: "plus")
}
.accessibilityIdentifier("identities.addMenu")
}
}
.sheet(isPresented: $showingLoadIdentity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ struct IdentityRow: View {
}
.padding(.vertical, 4)
}
.accessibilityIdentifier("identities.row.\(identity.identityIdBase58)")
}

private func refreshBalance() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ struct IdentityDetailView: View {
Text(identity.formattedBalance)
.foregroundColor(.blue)
.fontWeight(.medium)
.accessibilityIdentifier("identityDetail.balanceLabel")
// Display string is "%.8f DASH" — rounding hides
// sub-1000-credit deltas. Expose the raw credit
// count via accessibilityValue for tests that
// need exact numbers.
.accessibilityValue("\(UInt64(bitPattern: identity.balance))")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: UInt64(bitPattern: identity.balance) can mask negative-balance regressions through the new >0 assertion

PersistentIdentity.balance is Int64 while the SDK's credit balance is u64. accessibilityValue("\(UInt64(bitPattern: identity.balance))") reinterprets the bit pattern, so a negative Int64 (which itself would indicate an upstream FFI/persistence bug) silently surfaces as a huge positive UInt64. readIdentityBalanceCredits in WalletFlow.swift:700 then strips non-digits and parses, and CreditTransferTest.swift:138-142 only asserts XCTAssertGreaterThan(credits, 0, …). A negative-Int64 regression would (a) reinterpret to ~Int64.max * 2, (b) survive the digit filter, and (c) pass the floor — defeating the new regression-detection guard described in the in-file comment. Options: emit String(identity.balance) and reject a leading - in the parser; add an upper-bound sanity check (legitimate testnet balances are nowhere near Int64.max); or assert non-negativity at the FFI boundary so views never see a negative Int64.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift`:
- [SUGGESTION] lines 139-147: `UInt64(bitPattern: identity.balance)` can mask negative-balance regressions through the new `>0` assertion
  `PersistentIdentity.balance` is `Int64` while the SDK's credit balance is `u64`. `accessibilityValue("\(UInt64(bitPattern: identity.balance))")` reinterprets the bit pattern, so a negative `Int64` (which itself would indicate an upstream FFI/persistence bug) silently surfaces as a huge positive `UInt64`. `readIdentityBalanceCredits` in `WalletFlow.swift:700` then strips non-digits and parses, and `CreditTransferTest.swift:138-142` only asserts `XCTAssertGreaterThan(credits, 0, …)`. A negative-`Int64` regression would (a) reinterpret to ~`Int64.max * 2`, (b) survive the digit filter, and (c) pass the floor — defeating the new regression-detection guard described in the in-file comment. Options: emit `String(identity.balance)` and reject a leading `-` in the parser; add an upper-bound sanity check (legitimate testnet balances are nowhere near `Int64.max`); or assert non-negativity at the FFI boundary so views never see a negative `Int64`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: UInt64(bitPattern: identity.balance) reinterprets negative Int64 and bypasses the new >0 floor

PersistentIdentity.balance is stored as Int64 while the SDK's underlying credit balance is u64. Exposing it via .accessibilityValue("\(UInt64(bitPattern: identity.balance))") reinterprets the bit pattern, so a negative Int64 (which itself would already indicate an upstream FFI/persistence bug) silently surfaces as a huge positive UInt64. readIdentityBalanceCredits (WalletFlow.swift:696-708) then strips non-digit characters and parses, and CreditTransferTest.swift:137-142 only asserts XCTAssertGreaterThan(credits, 0, …). A regression that produces a negative Int64 would (a) reinterpret to ~UInt64.max, (b) survive the digit filter, and (c) pass the >0 floor — defeating the regression-detection guard the in-file comment promises. Options: emit String(identity.balance) and reject a leading - in the parser; add an upper-bound sanity check (legitimate testnet balances are nowhere near Int64.max); or assert non-negativity at the FFI boundary so views never see a negative Int64.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift`:
- [SUGGESTION] lines 142-147: `UInt64(bitPattern: identity.balance)` reinterprets negative Int64 and bypasses the new `>0` floor
  `PersistentIdentity.balance` is stored as `Int64` while the SDK's underlying credit balance is `u64`. Exposing it via `.accessibilityValue("\(UInt64(bitPattern: identity.balance))")` reinterprets the bit pattern, so a negative `Int64` (which itself would already indicate an upstream FFI/persistence bug) silently surfaces as a huge positive `UInt64`. `readIdentityBalanceCredits` (`WalletFlow.swift:696-708`) then strips non-digit characters and parses, and `CreditTransferTest.swift:137-142` only asserts `XCTAssertGreaterThan(credits, 0, …)`. A regression that produces a negative `Int64` would (a) reinterpret to ~`UInt64.max`, (b) survive the digit filter, and (c) pass the `>0` floor — defeating the regression-detection guard the in-file comment promises. Options: emit `String(identity.balance)` and reject a leading `-` in the parser; add an upper-bound sanity check (legitimate testnet balances are nowhere near `Int64.max`); or assert non-negativity at the FFI boundary so views never see a negative `Int64`.

}

// Top-up entry point. Hidden for purely-local rows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ struct OptionsView: View {
}
.pickerStyle(SegmentedPickerStyle())
.disabled(isSwitchingNetwork)
.accessibilityIdentifier("options.networkPicker")

if appState.currentNetwork == .regtest {
Toggle("Use Docker Setup", isOn: $appState.useDockerSetup)
Expand All @@ -84,23 +85,29 @@ struct OptionsView: View {
HStack {
Text("Network Status")
Spacer()
if isSwitchingNetwork {
HStack(spacing: 4) {
ProgressView()
.scaleEffect(0.8)
Text("Switching...")
Group {
if isSwitchingNetwork {
HStack(spacing: 4) {
ProgressView()
.scaleEffect(0.8)
Text("Switching...")
.font(.caption)
.foregroundColor(.secondary)
}
} else if appState.sdk != nil {
Label("Connected", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.secondary)
.foregroundColor(.green)
} else {
Label("Disconnected", systemImage: "xmark.circle.fill")
.font(.caption)
.foregroundColor(.red)
}
} else if appState.sdk != nil {
Label("Connected", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.green)
} else {
Label("Disconnected", systemImage: "xmark.circle.fill")
.font(.caption)
.foregroundColor(.red)
}
// Tests wait on this label transitioning to "Connected"
// after a network switch (signal-based, not sleep-based).
.accessibilityElement(children: .combine)
.accessibilityIdentifier("options.networkStatusLabel")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ struct SearchWalletsForIdentitiesView: View {
}
.pickerStyle(.menu)
.disabled(isSearching || hdWallets.count < 1)
.accessibilityIdentifier("searchWallets.walletPicker")
}
}
}
Expand All @@ -169,6 +170,7 @@ struct SearchWalletsForIdentitiesView: View {
Text("+\(finding.foundCount)")
.fontWeight(.semibold)
.foregroundColor(finding.foundCount > 0 ? .green : .secondary)
.accessibilityIdentifier("searchWallets.foundCountLabel")
}
if let err = finding.error {
// No `.lineLimit` — identity-derivation errors can
Expand Down Expand Up @@ -298,6 +300,7 @@ struct SearchWalletsForIdentitiesView: View {
|| selectedWalletId == nil
|| selectedManagedWallet == nil
)
.accessibilityIdentifier("searchWallets.searchButton")
if selectedWalletId != nil && selectedManagedWallet == nil {
Text("This wallet isn't loaded in the wallet manager yet. "
+ "Restore it from the Wallets tab and try again.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ struct TransitionDetailView: View {
ForEach(identities, id: \.identityIdBase58) { identity in
Text(identity.displayName)
.tag(identity.identityIdBase58)
.accessibilityIdentifier("transition.senderIdentityOption.\(identity.identityIdBase58)")
}
}
.pickerStyle(MenuPickerStyle())
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.accessibilityIdentifier("transition.senderIdentityPicker")
}
}
}
Expand All @@ -188,6 +190,7 @@ struct TransitionDetailView: View {
.foregroundColor(.white)
.cornerRadius(10)
.disabled(!enabled)
.accessibilityIdentifier("transition.executeButton")
}

private var resultView: some View {
Expand All @@ -197,6 +200,7 @@ struct TransitionDetailView: View {
.foregroundColor(isError ? .red : .green)
Text(isError ? "Error" : "Success")
.font(.headline)
.accessibilityIdentifier("transition.resultStatusLabel")
Spacer()
Button("Copy") {
UIPasteboard.general.string = resultText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ struct TransitionInputView: View {
}
}
.padding(.vertical, 4)
.accessibilityIdentifier("transition.input.\(input.name)")
}

@ViewBuilder
Expand Down Expand Up @@ -539,6 +540,7 @@ struct TransitionInputView: View {
.foregroundColor(.white)
.cornerRadius(8)
}
.accessibilityIdentifier("transition.input.\(input.name).manualEntryButton")
}
} else {
Picker("Select Identity", selection: $value) {
Expand All @@ -560,11 +562,13 @@ struct TransitionInputView: View {
useManualEntry = true
}
}
.accessibilityIdentifier("transition.input.\(input.name).recipientPicker")
}
} else {
VStack(alignment: .leading, spacing: 8) {
TextField("Enter recipient identity ID", text: $value)
.textFieldStyle(RoundedBorderTextFieldStyle())
.accessibilityIdentifier("transition.input.\(input.name).manualEntryField")

if !identities.isEmpty {
Button(action: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// CreditTransferTest.swift
// SwiftExampleAppUITests
//
// Imports a wallet from a known testnet mnemonic that already has a
// registered identity, runs identity discovery, and asserts that the
// expected identity surfaces with a non-zero balance. The credit-
// transfer assertion is deferred to a follow-up.
Comment thread
llbartekll marked this conversation as resolved.
Outdated
//
// Skipped automatically when the env var is unset, so the rest of the
// suite can run locally without test-network credentials.
//
// Env var:
// * UI_TEST_TESTNET_MNEMONIC — sender wallet's 12-word phrase
//
// Note on env var forwarding: a previous run on this branch showed that
// `xcodebuild test ENV=...` did not propagate env vars to the XCUITest
// runner — only the prefix form `TEST_RUNNER_<NAME>` reached the test
Comment on lines +14 to +22
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: PR description references UI_TEST_TESTNET_MNEMONIC; code/CI use UI_TEST_MNEMONIC

Code (CreditTransferTest.swift:65), the file header (line 18), the in-class comment (line 31), and the CI workflow (TEST_RUNNER_UI_TEST_MNEMONIC: ${{ secrets.UI_TEST_MNEMONIC }}) consistently use UI_TEST_MNEMONIC. The PR description still names the secret UI_TEST_TESTNET_MNEMONIC. Code/CI are internally consistent — only the PR description is wrong. Update it so anyone configuring the GitHub Actions secret uses the actual name.

source: ['claude-general', 'codex-general']

Comment on lines +14 to +22
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: PR description references UI_TEST_TESTNET_MNEMONIC; code and CI both use UI_TEST_MNEMONIC

Code (CreditTransferTest.swift:18, regeneration comment), the CI workflow (.github/workflows/swift-example-app-ui-smoke.yml), and the test README are internally consistent on UI_TEST_MNEMONIC. If the PR description still references UI_TEST_TESTNET_MNEMONIC, anyone provisioning the GitHub Actions secret from the PR body will configure the wrong name and the discovery test will silently XCTSkip on every nightly run. This is a PR-body fix, not a code change — update the description to match UI_TEST_MNEMONIC.

source: ['claude-general']

// process (Xcode strips the prefix). Try the unprefixed form first; if
// the env var doesn't reach the test, use the prefixed form.
Comment on lines +16 to +24
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: PR description references UI_TEST_TESTNET_MNEMONIC; code uses UI_TEST_MNEMONIC

The PR description names the secret UI_TEST_TESTNET_MNEMONIC, but the test reads ProcessInfo.processInfo.environment["UI_TEST_MNEMONIC"] (line 44), the file header documents UI_TEST_MNEMONIC (lines 16, 20-22), and .github/workflows/swift-example-app-ui-smoke.yml forwards TEST_RUNNER_UI_TEST_MNEMONIC: ${{ secrets.UI_TEST_MNEMONIC }}. Code is internally consistent; only the PR description is wrong. Update the PR description so anyone configuring the secret uses the correct name.

source: ['claude-general']

//

import XCTest

final class CreditTransferTest: XCTestCase {
/// The pre-registered identity behind the sender mnemonic. Discovery
/// must surface this exact ID — that's the regression check on the
/// discovery path.
private let expectedSenderIdentityIdBase58 = "3ou98WEERy6ExmmHWYWsFtyhgW8rmr1giceZYTFqdAAA"

/// walletId derived from the test mnemonic (deterministic). Lets us
/// detect an already-imported wallet from a prior run and reuse it
/// instead of failing on `Wallet operation: Wallet already exists`.
private let expectedSenderWalletIdHex = "2450ec6b6dc2b1b0476875305a6870dee743d47474e3838642b655b68a600793"
Comment on lines +49 to +57
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Hardcoded sender identityId/walletId silently couple to the rotating CI secret

expectedSenderIdentityIdBase58 and expectedSenderWalletIdHex are deterministic functions of the mnemonic stored in secrets.UI_TEST_MNEMONIC. Nothing in the repo links them: a future maintainer rotating the secret (e.g. after the funded identity is exhausted, or in response to the artifact-leak surface flagged below) will see this test fail with an opaque "identity row not found within 60s" timeout from waitForIdentityRow, with no breadcrumb that the constants here also need to be regenerated. Either drop the walletId/identityId expectations and rely on the looser "runIdentityDiscovery saw +N (N>0)" + "readIdentityBalanceCredits parses" properties already covered, or add a comment block here naming the regeneration steps (import the new mnemonic into a clean simulator, observe the resulting wallets.walletRow.<hex> identifier and the discovered identity's base58 ID, paste both here).

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 28-36: Hardcoded sender identityId/walletId silently couple to the rotating CI secret
  `expectedSenderIdentityIdBase58` and `expectedSenderWalletIdHex` are deterministic functions of the mnemonic stored in `secrets.UI_TEST_MNEMONIC`. Nothing in the repo links them: a future maintainer rotating the secret (e.g. after the funded identity is exhausted, or in response to the artifact-leak surface flagged below) will see this test fail with an opaque `"identity row not found within 60s"` timeout from `waitForIdentityRow`, with no breadcrumb that the constants here also need to be regenerated. Either drop the walletId/identityId expectations and rely on the looser "`runIdentityDiscovery` saw +N (N>0)" + "`readIdentityBalanceCredits` parses" properties already covered, or add a comment block here naming the regeneration steps (import the new mnemonic into a clean simulator, observe the resulting `wallets.walletRow.<hex>` identifier and the discovered identity's base58 ID, paste both here).


override func setUpWithError() throws {
continueAfterFailure = false
}

@MainActor
func testImportWalletAndDiscoverIdentity() throws {
guard let mnemonic = ProcessInfo.processInfo.environment["UI_TEST_TESTNET_MNEMONIC"],
!mnemonic.isEmpty
else {
throw XCTSkip("Set UI_TEST_TESTNET_MNEMONIC to run this test.")
}
XCTAssertEqual(
mnemonic.split(separator: " ").count,
12,
"UI_TEST_TESTNET_MNEMONIC must be a 12-word phrase."
)

let app = XCUIApplication()
app.launch()
failIfRecoveryPromptVisible(in: app, timeout: 2)

// Force testnet — the simulator may have been left on a non-testnet
// network by previous runs. Idempotent if already on Testnet.
switchAppNetworkToTestnet(in: app)
Comment thread
llbartekll marked this conversation as resolved.

openWalletsTab(in: app)

// Wallets restored from the persister on cold launch come back
// watch-only — the SwiftExampleApp comment in SwiftExampleAppApp.swift
// notes that biometric unlock to rehydrate signing keys is "future
// work". Identity discovery needs private keys, so a leftover
// wallet from a prior run is unusable. Delete it (if present) and
// re-import to get a hot, signing-capable wallet.
let existingRow = app.descendants(matching: .any)
.matching(identifier: "wallets.walletRow.\(expectedSenderWalletIdHex)")
.firstMatch
if existingRow.waitForExistence(timeout: 5) {
// accessibilityLabel == wallet.label, so .label gives us the name.
let staleName = existingRow.label
bestEffortDeleteWallet(named: staleName, in: app)
}
Comment thread
llbartekll marked this conversation as resolved.

let walletName = "ImportTransfer-\(UUID().uuidString.prefix(6))"
addTeardownBlock {
MainActor.assumeIsolated {
let cleanupApp = XCUIApplication()
cleanupApp.launch()
bestEffortDeleteWallet(named: walletName, in: cleanupApp)
cleanupApp.terminate()
}
}
importWallet(named: walletName, mnemonic: mnemonic, in: app)
assertWalletRowVisible(named: walletName, in: app, exists: true, timeout: 30)

openIdentitiesTab(in: app)
runIdentityDiscovery(forWalletNamed: walletName, in: app)

let senderRow = waitForIdentityRow(idBase58: expectedSenderIdentityIdBase58, in: app)
senderRow.tap()

let balance = readIdentityBalanceCredits(in: app)
XCTAssertGreaterThan(
balance,
0,
"Discovered identity should have a non-zero balance."
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
Loading
Loading