hellbender-wallet/birchUITests/ScreenshotTests.swift
Nick Klockenga 209750c4e5
Rebrand Wallet to Birch Wallet (#28)
* step one

* progress

* minor theme enhancements

* update screenshot and icon links in README.md

* update site link

* swiftformat fixes
2026-04-30 21:00:59 -04:00

632 lines
24 KiB
Swift

//
// ScreenshotTests.swift
// birchUITests
//
// Fastlane `snapshot` walker. Invoked by `bundle exec fastlane screenshots`
// (see fastlane/Fastfile). Walks the app from Welcome through the main tabs,
// calling `snapshot(...)` at each marketing stop.
//
// Kept separate from birchUITests.swift on purpose: the assertion-heavy
// setup test validates that the flow still works, and this test is purely for
// capturing images. The descriptor-import sequence is duplicated (not shared)
// so each test fails in isolation.
//
import XCTest
final class ScreenshotTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments += ["-UITesting"]
}
override func tearDownWithError() throws {
app = nil
}
@MainActor
func testScreenshotTour() {
setupSnapshot(app)
app.launch()
// MARK: 01 - Welcome
let getStarted = app.buttons["Get Started"]
XCTAssertTrue(getStarted.waitForExistence(timeout: 5), "Welcome screen should show 'Get Started' button")
snapshot("01-Welcome")
getStarted.tap()
// MARK: 02 - Wallet Setup (creation choice)
let walletSetupTitle = app.staticTexts["Wallet Setup"]
XCTAssertTrue(walletSetupTitle.waitForExistence(timeout: 3), "Wallet Setup screen should appear")
sleep(1)
snapshot("02-Wallet-Setup")
// MARK: Walk the descriptor-import flow to reach a loaded wallet.
// (Mirrors birchUITests.swift `testSetupWalletViaDescriptorImport`.)
let importCard = app.staticTexts["Import Descriptor"]
XCTAssertTrue(importCard.waitForExistence(timeout: 3), "Creation choice should show 'Import Descriptor' option")
importCard.tap()
let importTitle = app.staticTexts["Import Descriptor"]
XCTAssertTrue(importTitle.waitForExistence(timeout: 3), "Descriptor import screen should appear")
let testDescriptor = "wsh(sortedmulti(1,[7a13a7b1/48'/1'/0'/2']tpubDETciRzaZyqww2dSAyT2j6tWgzREyiZEY2iZDPKDtqNpSEqqFS31DZUFFTFnayx7wLUVYx3V1R2AWhhWbFrnCukKZ1kmnn83Fn2xSf7hEaH/<0;1>/*,[30a36b52/48'/1'/0'/2']tpubDF6MPv2vWsbCo8c7rk4X32BPa5yuj4niem5Pr6isrd9cSdCkYETcGUmBSFY4ekTR1CRFmjn4eoYGrwPU19FffwEpX7Tda6BBmg91aiHKpmE/<0;1>/*))"
let textEditor = app.textViews.firstMatch
XCTAssertTrue(textEditor.waitForExistence(timeout: 3), "Descriptor text editor should exist")
textEditor.tap()
// Paste the descriptor instead of typing character-by-character.
// typeText() is extremely slow on older simulators (e.g. iPhone 11 Pro
// Max) for 350+ character strings and can cause downstream timeouts.
UIPasteboard.general.string = testDescriptor
textEditor.press(forDuration: 1.2)
let pasteButton = app.menuItems["Paste"]
if pasteButton.waitForExistence(timeout: 3) {
pasteButton.tap()
} else {
// Fallback to typeText if paste menu doesn't appear
textEditor.typeText(testDescriptor)
}
// Dismiss keyboard by tapping a non-field area
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
sleep(1)
// MARK: 03 - Descriptor Import (filled)
snapshot("03-DescriptorImport")
let testnet4Button = app.buttons["Testnet4"]
if testnet4Button.waitForExistence(timeout: 5) {
testnet4Button.tap()
}
let importButton = app.buttons["Import"]
XCTAssertTrue(importButton.waitForExistence(timeout: 5), "Import button should exist")
importButton.tap()
// Descriptor parsing and BDK validation can take several seconds on
// slower simulators (e.g. iPhone 11 Pro Max on x86_64).
let nameTitle = app.staticTexts["Name Your Wallet"]
XCTAssertTrue(nameTitle.waitForExistence(timeout: 30), "Wallet name screen should appear")
let nameField = app.textFields["My Wallet"]
XCTAssertTrue(nameField.waitForExistence(timeout: 3), "Wallet name text field should exist")
nameField.tap()
nameField.typeText("Birch")
// In descriptor-import mode the button reads "Create Wallet" (not "Next")
// and skips the Review screen, going straight to the loaded wallet.
let createButton = app.buttons["Create Wallet"]
XCTAssertTrue(createButton.waitForExistence(timeout: 3), "Create Wallet button should exist")
createButton.tap()
// Wait for the main Transactions tab to render with a balance, then let
// Electrum sync catch up so the balance/tx list aren't stuck at zero.
let balanceExists = app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'sats'")).firstMatch
XCTAssertTrue(balanceExists.waitForExistence(timeout: 15), "Main screen should appear after wallet creation")
sleep(12)
// Enable "Show Fiat Price" in Settings before capturing Transactions
let settingsTabEarly = app.tabBars.buttons["Settings"]
XCTAssertTrue(settingsTabEarly.waitForExistence(timeout: 5), "Settings tab should exist")
settingsTabEarly.tap()
sleep(1)
let fiatToggle = app.switches["showFiatPriceToggle"]
XCTAssertTrue(fiatToggle.waitForExistence(timeout: 5), "Show Fiat Price toggle should exist in Settings")
if fiatToggle.value as? String == "0" {
// Tap the right edge of the row where the switch thumb lives. A plain
// fiatToggle.tap() lands in the center of the accessibility frame,
// which for a Toggle with a two-line VStack label can hit the label
// area without flipping the switch.
fiatToggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
sleep(1)
}
XCTAssertEqual(fiatToggle.value as? String, "1", "Show Fiat Price toggle should be on after tap")
// Return to Transactions tab
let transactionsTabEarly = app.tabBars.buttons["Transactions"]
XCTAssertTrue(transactionsTabEarly.waitForExistence(timeout: 5), "Transactions tab should exist")
transactionsTabEarly.tap()
// Give the fiat rates fetch (kicked off when the toggle flipped) time to
// complete so the balance hero renders the secondary fiat line.
sleep(5)
// MARK: 04 - Transactions (balance hero + tx list)
snapshot("04-Transactions")
// MARK: 05 - Wallet Picker (overlay on transactions screen)
let walletPicker = app.buttons["walletPicker"].firstMatch
if walletPicker.waitForExistence(timeout: 3) {
walletPicker.tap()
let walletsTitle = app.staticTexts["Wallets"]
XCTAssertTrue(walletsTitle.waitForExistence(timeout: 3), "Wallet picker overlay should appear")
sleep(1)
snapshot("05-WalletPicker")
// Dismiss by tapping the wallet picker button again
walletPicker.tap()
sleep(1)
}
// MARK: 06 - Transaction Detail (tap first received transaction)
let firstTxCell = app.cells.firstMatch
if firstTxCell.waitForExistence(timeout: 5) {
firstTxCell.tap()
let receivedLabel = app.staticTexts["Received"]
let sentLabel = app.staticTexts["Sent"]
let detailAppeared = receivedLabel.waitForExistence(timeout: 5) || sentLabel.waitForExistence(timeout: 2)
XCTAssertTrue(detailAppeared, "Transaction detail should show Received or Sent label")
sleep(1)
snapshot("06-TransactionDetail")
// Go back to transaction list
app.navigationBars.buttons.element(boundBy: 0).tap()
sleep(1)
}
// MARK: 07 - Dashboard sheet (via "..." overflow menu)
let walletMenu = app.buttons["walletMenu"].firstMatch
if walletMenu.waitForExistence(timeout: 3) {
walletMenu.tap()
let dashboardMenuItem = app.buttons["Dashboard"]
if dashboardMenuItem.waitForExistence(timeout: 3) {
dashboardMenuItem.tap()
// Give the sheet a beat to animate in.
sleep(1)
snapshot("07-Dashboard")
// Dismiss the sheet by swiping the window down.
app.windows.firstMatch.swipeDown(velocity: .fast)
}
}
// MARK: 08 - Receive
let receiveTab = app.tabBars.buttons["Receive"]
XCTAssertTrue(receiveTab.waitForExistence(timeout: 5), "Receive tab should exist")
receiveTab.tap()
let viewAllAddresses = app.buttons["View All Addresses"]
XCTAssertTrue(viewAllAddresses.waitForExistence(timeout: 10), "View All Addresses link should appear")
snapshot("08-Receive")
// MARK: 09 - Addresses
viewAllAddresses.tap()
let addressesTitle = app.navigationBars["Addresses"]
XCTAssertTrue(addressesTitle.waitForExistence(timeout: 10), "Addresses screen should appear")
snapshot("09-Addresses")
// MARK: 10 - Address Detail (tap first address)
let firstAddressCell = app.cells.firstMatch
if firstAddressCell.waitForExistence(timeout: 5) {
firstAddressCell.tap()
let copyAddressButton = app.buttons["Copy Address"]
XCTAssertTrue(copyAddressButton.waitForExistence(timeout: 5), "Address detail should show Copy Address button")
snapshot("10-AddressDetail")
// Go back to address list
app.navigationBars.buttons.element(boundBy: 0).tap()
sleep(1)
}
// MARK: 11 - Send (lands on recipients step)
let sendTab = app.tabBars.buttons["Send"]
XCTAssertTrue(sendTab.waitForExistence(timeout: 5), "Send tab should exist")
sendTab.tap()
// SendFlowView headline is a static text "Send" wait for it to avoid
// racing the tab animation.
_ = app.staticTexts["Send"].waitForExistence(timeout: 5)
snapshot("11-Send")
// MARK: 12 - UTXOs
let utxosTab = app.tabBars.buttons["UTXOs"]
XCTAssertTrue(utxosTab.waitForExistence(timeout: 5), "UTXOs tab should exist")
utxosTab.tap()
let utxosHeader = app.staticTexts["UTXOs"]
XCTAssertTrue(utxosHeader.waitForExistence(timeout: 5), "UTXOs header should appear")
sleep(1)
snapshot("12-UTXOs")
// MARK: 13 - UTXO Detail (tap first UTXO)
let firstUTXOCell = app.cells.firstMatch
if firstUTXOCell.waitForExistence(timeout: 5) {
firstUTXOCell.tap()
let utxoDetailTitle = app.navigationBars["UTXO Detail"]
XCTAssertTrue(utxoDetailTitle.waitForExistence(timeout: 5), "UTXO Detail screen should appear")
snapshot("13-UTXODetail")
// Go back to UTXO list
app.navigationBars.buttons.element(boundBy: 0).tap()
sleep(1)
}
// MARK: 14 - Settings
let settingsTab = app.tabBars.buttons["Settings"]
XCTAssertTrue(settingsTab.waitForExistence(timeout: 5), "Settings tab should exist")
settingsTab.tap()
sleep(1)
snapshot("14-Settings")
// MARK: Navigate to Transactions and open wallet picker
let transactionsTab = app.tabBars.buttons["Transactions"]
XCTAssertTrue(transactionsTab.waitForExistence(timeout: 5), "Transactions tab should exist")
transactionsTab.tap()
sleep(1)
let walletPickerBtn = app.buttons["walletPicker"].firstMatch
XCTAssertTrue(walletPickerBtn.waitForExistence(timeout: 3), "Wallet picker button should exist")
walletPickerBtn.tap()
let walletsTitleAdd = app.staticTexts["Wallets"]
XCTAssertTrue(walletsTitleAdd.waitForExistence(timeout: 3), "Wallet picker overlay should appear")
sleep(1)
let addWalletBtn = app.buttons["Add"]
XCTAssertTrue(addWalletBtn.waitForExistence(timeout: 3), "Add button should exist in wallet picker")
addWalletBtn.tap()
// Setup Wizard sheet opens Welcome step
let getStartedNew = app.buttons["Get Started"]
XCTAssertTrue(getStartedNew.waitForExistence(timeout: 5), "Welcome screen should show 'Get Started' button")
getStartedNew.tap()
// Creation choice tap "Create New Wallet"
let createNewCard = app.staticTexts["Create New Wallet"]
XCTAssertTrue(createNewCard.waitForExistence(timeout: 3), "Creation choice should show 'Create New Wallet' option")
createNewCard.tap()
// MARK: 15 - Multisig Configuration (Testnet4 default)
let multisigTitle = app.staticTexts["Multisig Configuration"]
XCTAssertTrue(multisigTitle.waitForExistence(timeout: 5), "Multisig Configuration screen should appear")
sleep(1)
snapshot("15-MultisigConfig-Testnet4")
// Switch to Mainnet
let mainnetSegBtn = app.segmentedControls.firstMatch.buttons["Mainnet"]
XCTAssertTrue(mainnetSegBtn.waitForExistence(timeout: 3), "Mainnet segment button should exist")
mainnetSegBtn.tap()
sleep(1)
// MARK: 16 - Multisig Configuration (Mainnet)
snapshot("16-MultisigConfig-Mainnet")
// Switch back to Testnet4
let testnet4SegBtn = app.segmentedControls.firstMatch.buttons["Testnet4"]
XCTAssertTrue(testnet4SegBtn.waitForExistence(timeout: 3), "Testnet4 segment button should exist")
testnet4SegBtn.tap()
sleep(1)
// Advance to cosigner import
let multisigNextBtn = app.buttons["Next"]
XCTAssertTrue(multisigNextBtn.waitForExistence(timeout: 3), "Next button should exist on multisig config screen")
multisigNextBtn.tap()
// MARK: 17 - Empty Cosigner Import Screen
let cosignerImportTitle = app.staticTexts["Import Cosigners"]
XCTAssertTrue(cosignerImportTitle.waitForExistence(timeout: 5), "Import Cosigners screen should appear")
sleep(1)
snapshot("17-CosignerImport-Empty")
// MARK: Fill Cosigner 1
// Type fingerprint into TextField, press Return to dismiss its keyboard.
// Then type xpub directly into the TextEditor (avoids the system clipboard
// permission prompt), and dismiss via swipeDown (scrollDismissesKeyboard).
// Do NOT press Return in the TextEditor it inserts a newline that would
// corrupt the xpub and fail BDK descriptor parsing.
let fpField1 = app.textFields["e.g. 73c5da0a"]
XCTAssertTrue(fpField1.waitForExistence(timeout: 3), "Fingerprint field should exist")
fpField1.tap()
fpField1.typeText("07d25f0c")
app.keyboards.buttons["Return"].tap()
let xpubEditor1 = app.textViews.firstMatch
XCTAssertTrue(xpubEditor1.waitForExistence(timeout: 3), "Xpub text editor should exist")
xpubEditor1.tap()
xpubEditor1.typeText("tpubDE2gU1F6b1GXDg2bFjeq6RUnBmAe2moTNG7x47Cga3VnVnm7EJWLdJE73ZL2MEwKTc2dLNeSudXUjexm2xJ5qboosbnEb1SEiGyJtJcqqZK")
// Dismiss keyboard by tapping a non-field area
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
sleep(1)
// MARK: 18 - Cosigner 1 Filled (no keyboard)
snapshot("18-CosignerImport-Cosigner1")
let nextCosignerBtn1 = app.buttons["Next Cosigner"]
XCTAssertTrue(nextCosignerBtn1.waitForExistence(timeout: 3), "Next Cosigner button should exist")
nextCosignerBtn1.tap()
sleep(1)
// MARK: Fill Cosigner 2
let fpField2 = app.textFields["e.g. 73c5da0a"]
XCTAssertTrue(fpField2.waitForExistence(timeout: 3), "Fingerprint field should exist for cosigner 2")
fpField2.tap()
fpField2.typeText("d73869a4")
app.keyboards.buttons["Return"].tap()
let xpubEditor2 = app.textViews.firstMatch
XCTAssertTrue(xpubEditor2.waitForExistence(timeout: 3), "Xpub text editor should exist for cosigner 2")
xpubEditor2.tap()
xpubEditor2.typeText("tpubDET5GnMK8Zr7UH63ni72etKd7ZYxVq8NvtSneNBfEDJ7YtnSHUmiPCaBYXzCdR6ZBKWvBMXT3urCVp7sLmG6z8VTpdFRJuW4VL7xjHdLFpY")
// Dismiss keyboard by tapping a non-field area. Do not use app.swipeDown()
// here: the setup wizard is inside a sheet, and a full-app swipe-down
// starts the sheet-dismiss gesture, leaving the sheet in a partially-
// dragged state where the subsequent "Next Cosigner" tap fails to advance.
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
sleep(1)
let nextCosignerBtn2 = app.buttons["Next Cosigner"]
XCTAssertTrue(nextCosignerBtn2.waitForExistence(timeout: 3), "Next Cosigner button should exist for cosigner 2")
nextCosignerBtn2.tap()
sleep(1)
// MARK: Fill Cosigner 3
// Verify we actually advanced to cosigner 3 before proceeding, so a future
// regression in the cosigner-2 -> cosigner-3 transition fails here rather
// than producing a mislabeled screenshot.
let cosigner3Header = app.staticTexts["Cosigner 3 of 3"]
XCTAssertTrue(cosigner3Header.waitForExistence(timeout: 3), "Should have advanced to Cosigner 3 of 3")
let fpField3 = app.textFields["e.g. 73c5da0a"]
XCTAssertTrue(fpField3.waitForExistence(timeout: 3), "Fingerprint field should exist for cosigner 3")
fpField3.tap()
fpField3.typeText("e3870581")
app.keyboards.buttons["Return"].tap()
let xpubEditor3 = app.textViews.firstMatch
XCTAssertTrue(xpubEditor3.waitForExistence(timeout: 3), "Xpub text editor should exist for cosigner 3")
xpubEditor3.tap()
xpubEditor3.typeText("tpubDF3GwUrMb5WkigsDUpUWUADH55G3Ez771QujmFqeyrNEPD7onkqTwCsCEjNRbSrbD9VYKDfMHfg7bajem5aEX7CyMp2q5fvQzacy75bUesQ")
// Dismiss keyboard by tapping a non-field area
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
sleep(1)
// MARK: 19 - Cosigner 3 Filled (no keyboard)
snapshot("19-CosignerImport-Cosigner3")
let continueBtn = app.buttons["Continue"]
XCTAssertTrue(continueBtn.waitForExistence(timeout: 3), "Continue button should exist")
continueBtn.tap()
// MARK: 20 - Wallet Name
let nameWalletTitle = app.staticTexts["Name Your Wallet"]
XCTAssertTrue(nameWalletTitle.waitForExistence(timeout: 10), "Wallet name screen should appear")
let newWalletNameField = app.textFields["My Wallet"]
XCTAssertTrue(newWalletNameField.waitForExistence(timeout: 3), "Wallet name text field should exist")
newWalletNameField.tap()
newWalletNameField.typeText("My New Wallet")
app.swipeDown()
sleep(1)
snapshot("20-WalletName")
let walletNameNextBtn = app.buttons["Next"]
XCTAssertTrue(walletNameNextBtn.waitForExistence(timeout: 3), "Next button should exist on wallet name screen")
walletNameNextBtn.tap()
// MARK: 21 - Verify Wallet (top summary + cosigners)
let verifyWalletTitle = app.staticTexts["Verify Wallet"]
XCTAssertTrue(verifyWalletTitle.waitForExistence(timeout: 30), "Verify Wallet screen should appear")
sleep(2)
snapshot("21-VerifyWallet-Top")
// Scroll up a controlled amount to land at the "Back Up Your Descriptor"
// section. swipeUp(velocity: .slow) overshoots by ~60pt, so use a
// fixed-distance coordinate drag instead.
let dragStart = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.72))
let dragEnd = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.42))
dragStart.press(forDuration: 0.05, thenDragTo: dragEnd)
sleep(1)
// MARK: 22 - Verify Wallet (backup section)
snapshot("22-VerifyWallet-Backup")
// Scroll to bring "Verify Receive Address" section to the top
app.swipeUp()
sleep(1)
// MARK: 23 - Verify Wallet (receive address section)
snapshot("23-VerifyWallet-Verify")
// Tap "Create Wallet"
let createWalletFinalBtn = app.buttons["Create Wallet"]
XCTAssertTrue(createWalletFinalBtn.waitForExistence(timeout: 5), "Create Wallet button should exist")
createWalletFinalBtn.tap()
// MARK: 24 - New Wallet syncing
// Capture the transaction screen ~3 seconds into the sync (sheet animates
// away in ~1s, then sync starts total sleep of 4s lands mid-sync).
snapshot("24-NewWalletLoading")
// MARK: - Send Flow Screenshots
// Navigate to Send tab
let sendTabFlow = app.tabBars.buttons["Send"]
XCTAssertTrue(sendTabFlow.waitForExistence(timeout: 5), "Send tab should exist")
sendTabFlow.tap()
_ = app.staticTexts["Send"].waitForExistence(timeout: 5)
sleep(1)
// Dismiss any resume signing card if present
let noBtn = app.buttons["No"]
if noBtn.waitForExistence(timeout: 2) {
noBtn.tap()
sleep(1)
}
// MARK: Fill Recipient 1
// Type address directly into the address field (do not use Paste button)
let addressField = app.textFields.matching(NSPredicate(format: "placeholderValue CONTAINS 'tb1'")).firstMatch
XCTAssertTrue(addressField.waitForExistence(timeout: 5), "Address text field should exist")
addressField.tap()
addressField.typeText("tb1qkmp8r90rcqpzdm6uqy2034j30csd902ynk35pezwg3sag6604xystkkazg")
// Dismiss keyboard
app.swipeDown()
sleep(1)
// Type label
let labelField = app.textFields["Label (optional)"]
XCTAssertTrue(labelField.waitForExistence(timeout: 3), "Label field should exist")
labelField.tap()
labelField.typeText("Test Transaction")
// Dismiss keyboard
app.swipeDown()
sleep(1)
// Type sats amount
let amountField = app.textFields["0"]
XCTAssertTrue(amountField.waitForExistence(timeout: 3), "Amount field should exist")
amountField.tap()
amountField.typeText("71234")
// Dismiss keyboard
app.swipeDown()
sleep(1)
// Expand Fee card by tapping the fee header area
let feeLabel = app.staticTexts["Fee"]
XCTAssertTrue(feeLabel.waitForExistence(timeout: 3), "Fee label should exist")
feeLabel.tap()
sleep(1)
// Select Custom fee
let customLabel = app.staticTexts["Custom"]
XCTAssertTrue(customLabel.waitForExistence(timeout: 3), "Custom fee option should exist")
customLabel.tap()
sleep(1)
// Type custom fee rate
let customFeeField = app.textFields["0.0"]
XCTAssertTrue(customFeeField.waitForExistence(timeout: 3), "Custom fee text field should exist")
customFeeField.tap()
// Clear any existing text and type new value
customFeeField.typeText("2.5")
// Dismiss keyboard
app.swipeDown()
sleep(1)
// Collapse fee card by tapping the fee header again
feeLabel.tap()
sleep(1)
// MARK: 25 - Send Recipients Filled
snapshot("25-SendRecipientsFilled")
// MARK: Tap Review
let reviewButton = app.buttons["Review"]
XCTAssertTrue(reviewButton.waitForExistence(timeout: 5), "Review button should exist")
reviewButton.tap()
// Wait for the Review Transaction screen
let reviewTitle = app.staticTexts["Review Transaction"]
XCTAssertTrue(reviewTitle.waitForExistence(timeout: 15), "Review Transaction screen should appear")
sleep(1)
// MARK: 26 - Review Transaction (top)
snapshot("26-ReviewTransaction-Top")
// Scroll to the bottom of the review screen
app.swipeUp()
sleep(1)
// MARK: 27 - Review Transaction (bottom)
snapshot("27-ReviewTransaction-Bottom")
// Tap "Show QR for Signing"
let showQRBtn = app.buttons["Show QR for Signing"]
XCTAssertTrue(showQRBtn.waitForExistence(timeout: 5), "Show QR for Signing button should exist")
showQRBtn.tap()
// Wait for the PSBT Display / signing QR screen
let scanSignedBtn = app.buttons["Scan Signed PSBT"]
XCTAssertTrue(scanSignedBtn.waitForExistence(timeout: 15), "Scan Signed PSBT button should appear on QR display")
sleep(2)
// MARK: 28 - PSBT QR Display (animated QR showing)
snapshot("28-PSBTQRDisplay")
// Expand Advanced section
let advancedToggle = app.staticTexts["Advanced"]
XCTAssertTrue(advancedToggle.waitForExistence(timeout: 3), "Advanced disclosure group should exist")
advancedToggle.tap()
sleep(1)
// Quarter-scroll to show Advanced settings below the QR
let qtrStart = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.75))
let qtrEnd = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.50))
qtrStart.press(forDuration: 0.05, thenDragTo: qtrEnd)
sleep(1)
// MARK: 29 - PSBT QR Display (Advanced expanded)
snapshot("29-PSBTQRDisplay-Advanced")
// Tap "Scan Signed PSBT" button to go to scan screen
let scanBtn = app.buttons["Scan Signed PSBT"]
XCTAssertTrue(scanBtn.waitForExistence(timeout: 5), "Scan Signed PSBT button should exist")
scanBtn.tap()
// Wait for the Scan Signed PSBT screen
let scanTitle = app.staticTexts["Scan Signed PSBT"]
XCTAssertTrue(scanTitle.waitForExistence(timeout: 5), "Scan Signed PSBT screen should appear")
sleep(1)
// MARK: 30 - Scan Signed PSBT Screen
snapshot("30-ScanSignedPSBT")
// Go back to QR Display
let backToQR = app.buttons["Back to QR Display"]
XCTAssertTrue(backToQR.waitForExistence(timeout: 3), "Back to QR Display button should exist")
backToQR.tap()
sleep(1)
// Tap "Save PSBT"
let savePSBTBtn = app.buttons["Save PSBT"]
XCTAssertTrue(savePSBTBtn.waitForExistence(timeout: 5), "Save PSBT button should exist")
savePSBTBtn.tap()
// Wait for the Save PSBT alert to appear
let saveAlert = app.alerts["Save PSBT"]
XCTAssertTrue(saveAlert.waitForExistence(timeout: 5), "Save PSBT alert should appear")
sleep(1)
// MARK: 31 - Save PSBT Dialog
snapshot("31-SavePSBT")
}
}