diff --git a/.claude/plans/2026-08-20-ui-test-tab-bar-rewrite.md b/.claude/plans/2026-08-20-ui-test-tab-bar-rewrite.md index f8eb78222..e778d4d21 100644 --- a/.claude/plans/2026-08-20-ui-test-tab-bar-rewrite.md +++ b/.claude/plans/2026-08-20-ui-test-tab-bar-rewrite.md @@ -12,30 +12,45 @@ call site. | v1 affordance | v2 route | |---|---| -| `ScanTopBar` Settings button (`app.buttons["Settings"]`) | You tab (`app.buttons["You"]`) → `YouScreen` settings list | +| `ScanTopBar` Settings button (`app.buttons["Settings"]`) | You tab (`app.buttons["You"]`) → `YouScreen` settings list ("My Account", "Advanced"); **Log Out is under Advanced** | | `ScanBottomBar` Cash button (`app.buttons["Cash"]`) | no scanner give entry; per-currency **Give** on `CurrencyInfoScreen`, or the `flipcash://give` deeplink | | `scan-wallet-button` | Wallet tab (`app.buttons["Wallet"]`) | | `scan-tips-button` | Chat tab (`app.buttons["Chat"]`) — embedded, so no `navigationBars["Tips"]` Close button | | `scan-discover-button` | Wallet tab → "Discover Currencies" tile (a push, not a sheet) | | `discover-create-currency-card` promo | Wallet tab → "Create a Currency" tile — `CurrencyDiscoveryScreen.hidesPromo` hides the card | | Settings "Add Money" / "Withdraw Money" rows | Wallet tab tiles of the same name | +| `CurrencyInfoScreen` "Buy" / "Sell" / "Give" footer | `CurrencyInfoContentV2` action tiles — **Give / Convert / Withdraw** for a currency you hold, **Get** for one you don't. No Buy, no Sell. | ## Gotchas - **The tab bar hides on push.** `HomeTabView.isTabBarHidden` is true when a card is expanded, a bill is showing, or the active tab's stack is non-empty. So `assertMainScreenReached()` (now the Wallet tab) only holds at a tab root — pop first. -- **The You tab gates on a tip profile.** `YouScreen` (and therefore the whole settings - list) only renders when `session.profile?.isTippable == true`; otherwise the tab shows - `TipCardSetupPrompt`. Fresh-account tests cannot reach Settings this way. +- **The You tab's settings rows sit below the fold.** They render under the tip card, so + scroll them into view (`scrollUpToAndTap(_:in:)`) rather than tapping blind. - **`GiveDiscoverGateRegressionTests` tests behavior that no longer exists.** USDF is giveable now (`BetaFlags.allowsDollarsGive`), so `GiveCashGate.discoverCurrencies` is unreachable and the "No Community Currencies Yet" dialog never shows. Delete the test with the phase-2 teardown rather than rewriting it. - **`BaseUITestCase.navigateToGiveAmount()` still taps the v1 Cash button.** Its only callers are skipped; rewrite it against whichever give entry the tests should cover. - -## Product gap worth confirming separately - -Settings is reachable *only* from the You tab, which requires a tippable profile. An -account without one appears to have no route to Settings at all. +- **A wallet card opens `CurrencyInfoScreen` as an overlay, not a push.** `TokenCardStack` + reports the tap through `onCardTap`; the wallet lifts the page over the deck with a + close box instead of a back chevron, so there is no `navigationBars` entry to wait on. +- **The v1 buy/sell entries are gone from an owned currency.** `CurrencyInfoContentV2` + gates its tiles on `isOwned` (balance has displayable value), and the owned branch is + Give/Convert/Withdraw. Buying more of a currency you already hold has no entry point + there, and buy pushes `.buyCurrency(mint)` onto the stack rather than presenting the + `.buy` nested sheet the reserves test asserts. Deciding what the v2 equivalents should + assert is a product question, not a selector swap — `BuyReservesRegressionTests`, + `BuyWithCurrencyRegressionTests` and `CurrencySellRegressionTests` are skipped on that. + +## Rewritten so far + +- `LoginSmokeTests.testRelogin_viaAccountSelection` — You tab → Advanced → Log Out. +- `BuyApplePayRegressionTests`, `BuyDepositRegressionTests`, `BuyPhantomRegressionTests` + — enter through the Wallet tab's `wallet-tile-add-money` tile. The picker's heading is + **"Add Money With"** in v2, not "Select Method", and the debit-card row runs the + verified-contact gate *before* "Amount to Add". +- `WalletUsdfRowRegressionTests` — the v2 deck's cards carry `currency-row` / + `currency-row-usdf`, so the wallet page object matches again. diff --git a/Flipcash/Core/Screens/Main/Home/TokenCardStack.swift b/Flipcash/Core/Screens/Main/Home/TokenCardStack.swift index 210be7371..f920c7fc2 100644 --- a/Flipcash/Core/Screens/Main/Home/TokenCardStack.swift +++ b/Flipcash/Core/Screens/Main/Home/TokenCardStack.swift @@ -103,6 +103,9 @@ struct TokenCardStack: View { TokenCardView(data: item, height: cardHeight) } .buttonStyle(.plain) + // USDF carries its own identifier so a test can target an + // investable token without depending on where USDF sorts. + .accessibilityIdentifier(item.isUSDF ? "currency-row-usdf" : "currency-row") // Resting fan, and the reorganisation when a card is opened, are // both expressed here so they interpolate as one animation. .opacity(item.mint == hiddenMint ? 0 : 1) diff --git a/Flipcash/Core/Screens/Main/Home/WalletScreen.swift b/Flipcash/Core/Screens/Main/Home/WalletScreen.swift index fb471f300..721068bb3 100644 --- a/Flipcash/Core/Screens/Main/Home/WalletScreen.swift +++ b/Flipcash/Core/Screens/Main/Home/WalletScreen.swift @@ -562,22 +562,22 @@ private struct WalletScreenContent: View { // `.deposit`/`.withdraw` are the app's canonical design-system // glyphs for these actions (see Settings' `.card(icon:)` buttons) — // a matched arrow-to-baseline pair, not SF Symbols. - walletTile(icon: .asset(.deposit), title: "Add Money") { + walletTile(icon: .asset(.deposit), title: "Add Money", identifier: "wallet-tile-add-money") { router.presentAddMoney(.general, source: .balance) } // `.withdrawCurrency(nil)` (not `.withdraw`) so the flow pops back // to the wallet's own stack on finish — `.withdraw` hardcodes a // return to the settings stack and would strand the user here. A // nil mint opens the currency picker (all balances). - walletTile(icon: .asset(.withdraw), title: "Withdraw Money") { + walletTile(icon: .asset(.withdraw), title: "Withdraw Money", identifier: "wallet-tile-withdraw-money") { router.push(.withdrawCurrency(nil)) } } HStack(spacing: 12) { - walletTile(icon: .symbol("globe"), title: "Discover Currencies") { + walletTile(icon: .symbol("globe"), title: "Discover Currencies", identifier: "wallet-tile-discover-currencies") { router.push(.discoverCurrencies) } - walletTile(icon: .asset(.coinsAdd), title: "Create a Currency") { + walletTile(icon: .asset(.coinsAdd), title: "Create a Currency", identifier: "wallet-tile-create-currency") { router.push(.currencyCreationSummary) } } @@ -593,7 +593,11 @@ private struct WalletScreenContent: View { /// A tile-style entry point: icon top-leading, label pinned bottom-leading, /// inside a translucent rounded card. They tile two-up per row. - private func walletTile(icon: TileGlyph, title: String, action: @escaping () -> Void) -> some View { + /// + /// `identifier` is what tests target: several of these titles also appear on + /// the new-user tutorial rows above, where a completed milestone renders + /// disabled, so matching on the label alone can resolve to an untappable row. + private func walletTile(icon: TileGlyph, title: String, identifier: String, action: @escaping () -> Void) -> some View { Button(action: action) { VStack(alignment: .leading, spacing: 0) { tileGlyph(icon) @@ -612,6 +616,7 @@ private struct WalletScreenContent: View { .clipShape(RoundedRectangle(cornerRadius: Metrics.buttonRadius, style: .continuous)) } .buttonStyle(.plain) + .accessibilityIdentifier(identifier) } /// Renders a `TileGlyph` at a consistent ~24pt: SF Symbols sized by font, diff --git a/FlipcashUITests/Regression/AddMoneyGateRegressionTests.swift b/FlipcashUITests/Regression/AddMoneyGateRegressionTests.swift index 4a30401fb..b1a3cce5d 100644 --- a/FlipcashUITests/Regression/AddMoneyGateRegressionTests.swift +++ b/FlipcashUITests/Regression/AddMoneyGateRegressionTests.swift @@ -35,10 +35,10 @@ final class AddMoneyGateRegressionTests: BaseUITestCase { "The buy amount sheet must open even when the account has no balance" ) - // Add Money → the Select Method picker. This flow enters from + // Add Money → the Add Money With picker. This flow enters from // Discover, so the sheet's CTA is the only Add Money button on screen. waitUntilHittableAndTap(app.buttons["Add Money"].firstMatch) - addMoney.assertSelectMethodReached() + addMoney.assertMethodPickerReached() } func testCreateCurrencyWithNoAssets_gatesOnAddMoney() throws { @@ -67,8 +67,8 @@ final class AddMoneyGateRegressionTests: BaseUITestCase { "Expected the create-context subtitle on the No Balance prompt" ) - // Add Money → the Select Method picker. + // Add Money → the Add Money With picker. addMoney.tapAddMoney(from: self) - addMoney.assertSelectMethodReached() + addMoney.assertMethodPickerReached() } } diff --git a/FlipcashUITests/Regression/BuyApplePayRegressionTests.swift b/FlipcashUITests/Regression/BuyApplePayRegressionTests.swift index 8de0c7fda..f71e219e0 100644 --- a/FlipcashUITests/Regression/BuyApplePayRegressionTests.swift +++ b/FlipcashUITests/Regression/BuyApplePayRegressionTests.swift @@ -5,23 +5,25 @@ import XCTest -/// Regression test for the Add Money Apple Pay (Coinbase) path's -/// verified-contact gate: an unverified account tapping Add Money must land -/// on the verification sheet's first step, not the Apple Pay overlay. Which -/// step shows (phone vs email) depends on the account's server-side phone -/// state, so the test accepts either. Stops short of completing -/// verification — SMS / email links are out of scope for the simulator. +/// Regression test for the Add Money debit-card (Coinbase) path's +/// verified-contact gate: an unverified account picking Debit Card must land +/// on the verification flow's first step, not on an amount screen or the +/// Apple Pay overlay. The gate runs up front — before "Amount to Add" — so +/// no empty screen appears ahead of it. Which step shows (phone vs email) +/// depends on the account's server-side phone state, so the test accepts +/// either. Stops short of completing verification — SMS / email links are +/// out of scope for the simulator. /// -/// Entry is the Balance screen's own Add Money button — buy entry is capped +/// Entry is the Wallet tab's own Add Money tile — buy entry is capped /// at the highest spendable balance, so the old buy-shortfall vehicle into /// Add Money no longer exists. /// /// **Prerequisites:** /// - `FLIPCASH_UI_TEST_ACCESS_KEY` set in `secrets.local.xcconfig` /// - The account behind the access key must have **no verified email**. A -/// verified email skips the gate and routes straight to Apple Pay, which -/// would fail this assertion. -/// - The account must have the Coinbase onramp enabled, or the Pay +/// verified email skips the gate and routes straight to "Amount to Add", +/// which would fail this assertion. +/// - The account must have the Coinbase onramp enabled, or the Debit Card /// row is hidden. final class BuyApplePayRegressionTests: BaseUITestCase { @@ -29,25 +31,19 @@ final class BuyApplePayRegressionTests: BaseUITestCase { func testApplePay_unverifiedAccount_showsVerificationSheet() { let wallet = WalletScreen(app: app) - let amountEntry = AmountEntryScreen(app: app) let addMoney = AddMoneyStartScreen(app: app) let verifyInfo = VerifyInfoUIScreen(app: app) assertMainScreenReached() - // Navigate: Main → Wallet → Add Money → Select Method → Pay. + // Navigate: Wallet tab → Add Money tile → Add Money With → Debit Card. wallet.open(from: self) - waitUntilHittableAndTap(app.buttons["Add Money"].firstMatch) - addMoney.assertSelectMethodReached() + wallet.tapAddMoneyTile(from: self) + addMoney.assertMethodPickerReached() addMoney.selectPayDebitCard(from: self) - // Amount to Add → enter $10 → the verified-contact gate opens the - // verification sheet. - addMoney.assertAmountToAddReached() - amountEntry.keypadButton("1").tap() - amountEntry.keypadButton("0").tap() - waitUntilHittableAndTap(addMoney.amountToAddActionButton) - + // The gate fires on selection, so the verification step is the next + // screen — there is no amount entry to step through first. verifyInfo.assertVerificationStepReached() } } diff --git a/FlipcashUITests/Regression/BuyDepositRegressionTests.swift b/FlipcashUITests/Regression/BuyDepositRegressionTests.swift index 4186249c3..b228a029a 100644 --- a/FlipcashUITests/Regression/BuyDepositRegressionTests.swift +++ b/FlipcashUITests/Regression/BuyDepositRegressionTests.swift @@ -10,13 +10,13 @@ import XCTest /// the USDC education pre-flight; Next pushes the deposit-address screen. /// Exercises: /// -/// - The Balance screen's Add Money button opens "Select Method". +/// - The Wallet tab's Add Money tile opens the "Add Money With" picker. /// - Selecting Other Wallet shows the USDC education screen; Next pushes the /// deposit-address screen with the Copy Address button hittable. The address /// is derived from the session's owner key — its exact value isn't asserted, /// just that the CTA renders. /// -/// Entry is the Balance screen's own Add Money button — buy entry is capped +/// Entry is the Wallet tab's own Add Money tile — buy entry is capped /// at the highest spendable balance, so the old buy-shortfall vehicle into /// Add Money no longer exists. /// @@ -34,11 +34,11 @@ final class BuyDepositRegressionTests: BaseUITestCase { assertMainScreenReached() - // Navigate: Main → Wallet → Add Money → Select Method → Other Wallet → + // Navigate: Wallet tab → Add Money tile → Add Money With → Other Wallet → // USDC education pre-flight → Next → USDC deposit-address screen. wallet.open(from: self) - waitUntilHittableAndTap(app.buttons["Add Money"].firstMatch) - addMoney.assertSelectMethodReached() + wallet.tapAddMoneyTile(from: self) + addMoney.assertMethodPickerReached() addMoney.selectOtherWallet(from: self) education.assertReached() diff --git a/FlipcashUITests/Regression/BuyPhantomRegressionTests.swift b/FlipcashUITests/Regression/BuyPhantomRegressionTests.swift index 642b99e7f..ef44d7b11 100644 --- a/FlipcashUITests/Regression/BuyPhantomRegressionTests.swift +++ b/FlipcashUITests/Regression/BuyPhantomRegressionTests.swift @@ -9,7 +9,7 @@ import XCTest /// Exercises the in-app flow as far as can be tested without a real Phantom /// install: /// -/// - The Balance screen's Add Money button opens "Select Method". +/// - The Wallet tab's Add Money tile opens the "Add Money With" picker. /// - Selecting Phantom opens the "Add Money With Phantom" education screen /// with the "Connect Your Phantom Wallet" CTA. /// @@ -17,7 +17,7 @@ import XCTest /// deeplink, and only a successful connect pushes "Amount to Add" — out of /// scope for the local simulator without a real Phantom install. /// -/// Entry is the Balance screen's own Add Money button — buy entry is capped +/// Entry is the Wallet tab's own Add Money tile — buy entry is capped /// at the highest spendable balance, so the old buy-shortfall vehicle into /// Add Money no longer exists. /// @@ -33,10 +33,10 @@ final class BuyPhantomRegressionTests: BaseUITestCase { assertMainScreenReached() - // Navigate: Main → Wallet → Add Money → Select Method → Phantom. + // Navigate: Wallet tab → Add Money tile → Add Money With → Phantom. wallet.open(from: self) - waitUntilHittableAndTap(app.buttons["Add Money"].firstMatch) - addMoney.assertSelectMethodReached() + wallet.tapAddMoneyTile(from: self) + addMoney.assertMethodPickerReached() addMoney.selectPhantom(from: self) addMoney.assertPhantomEducationReached() diff --git a/FlipcashUITests/Regression/BuyReservesRegressionTests.swift b/FlipcashUITests/Regression/BuyReservesRegressionTests.swift index d8b79adc8..be6502e05 100644 --- a/FlipcashUITests/Regression/BuyReservesRegressionTests.swift +++ b/FlipcashUITests/Regression/BuyReservesRegressionTests.swift @@ -11,7 +11,7 @@ import XCTest /// - Next pushes the Select Payment Currency step; picking USDF lands on the /// Buy summary in its simple (no fee breakdown) variant. /// - A covered entry routes straight to the swap-processing screen (the Add -/// Money "Select Method" sheet never appears). +/// Money "Add Money With" picker never appears). /// - After OK on the processing screen, the user lands back on /// CurrencyInfoScreen — not the Wallet root, not the Scanner. /// @@ -24,7 +24,11 @@ final class BuyReservesRegressionTests: BaseUITestCase { override var requiresAuthentication: Bool { true } - func testBuyCurrency_fullFlowWithReserves() { + func testBuyCurrency_fullFlowWithReserves() throws { + try skipPendingTabBarRewrite( + "an owned currency's info page offers Give/Convert/Withdraw in the tab-bar UI — there is no Buy, and the buy flow pushes instead of opening a nested sheet" + ) + let wallet = WalletScreen(app: app) let currencyInfo = CurrencyInfoUIScreen(app: app) let amountEntry = AmountEntryScreen(app: app) @@ -57,9 +61,9 @@ final class BuyReservesRegressionTests: BaseUITestCase { waitUntilHittableAndTap(confirmation.buyButton) // A covered amount must not detour through the Add Money flow — the - // "Select Method" sheet must never appear. + // "Add Money With" picker must never appear. XCTAssertFalse( - app.staticTexts["Select Method"].waitForExistence(timeout: 2), + app.staticTexts["Add Money With"].waitForExistence(timeout: 2), "A covered amount must route straight to the swap, not the Add Money sheet" ) diff --git a/FlipcashUITests/Regression/BuyWithCurrencyRegressionTests.swift b/FlipcashUITests/Regression/BuyWithCurrencyRegressionTests.swift index 3b8252c7c..a632efedd 100644 --- a/FlipcashUITests/Regression/BuyWithCurrencyRegressionTests.swift +++ b/FlipcashUITests/Regression/BuyWithCurrencyRegressionTests.swift @@ -22,7 +22,11 @@ final class BuyWithCurrencyRegressionTests: BaseUITestCase { /// Buys $0.01 of the first wallet currency paying with another launchpad /// token, all the way through the processing screen. Moves ~$0.01 of real /// dev-environment value per run. - func testBuyCurrency_payingWithToken_fullFlow() { + func testBuyCurrency_payingWithToken_fullFlow() throws { + try skipPendingTabBarRewrite( + "an owned currency's info page offers Give/Convert/Withdraw in the tab-bar UI — there is no Buy, and the buy flow pushes instead of opening a nested sheet" + ) + let wallet = WalletScreen(app: app) let currencyInfo = CurrencyInfoUIScreen(app: app) let amountEntry = AmountEntryScreen(app: app) diff --git a/FlipcashUITests/Regression/CurrencySellRegressionTests.swift b/FlipcashUITests/Regression/CurrencySellRegressionTests.swift index 0eadefebe..7eddf9dd7 100644 --- a/FlipcashUITests/Regression/CurrencySellRegressionTests.swift +++ b/FlipcashUITests/Regression/CurrencySellRegressionTests.swift @@ -14,7 +14,11 @@ final class CurrencySellRegressionTests: BaseUITestCase { override var requiresAuthentication: Bool { true } - func testSellCurrency_fullFlowWithConfirmation() { + func testSellCurrency_fullFlowWithConfirmation() throws { + try skipPendingTabBarRewrite( + "Sell is Convert in the tab-bar UI, pushed onto the wallet stack rather than presented as the sell sheet this asserts" + ) + let wallet = WalletScreen(app: app) let currencyInfo = CurrencyInfoUIScreen(app: app) let amountEntry = AmountEntryScreen(app: app) diff --git a/FlipcashUITests/Smoke/DepositSmokeTests.swift b/FlipcashUITests/Smoke/DepositSmokeTests.swift index b9f98cef1..6ae6b4ce2 100644 --- a/FlipcashUITests/Smoke/DepositSmokeTests.swift +++ b/FlipcashUITests/Smoke/DepositSmokeTests.swift @@ -63,7 +63,7 @@ final class DepositSmokeTests: BaseUITestCase { let addMoney = AddMoneyStartScreen(app: app) settings.open(from: self) waitAndTap(settings.addMoneyButton) - addMoney.assertSelectMethodReached() + addMoney.assertMethodPickerReached() addMoney.selectOtherWallet(from: self) } } diff --git a/FlipcashUITests/Smoke/LoginSmokeTests.swift b/FlipcashUITests/Smoke/LoginSmokeTests.swift index 3c680c7fe..bb179ddc2 100644 --- a/FlipcashUITests/Smoke/LoginSmokeTests.swift +++ b/FlipcashUITests/Smoke/LoginSmokeTests.swift @@ -10,7 +10,7 @@ final class LoginSmokeTests: BaseUITestCase { func testLoginViaAccessKey_reachesMainScreen() { assertMainScreenReached( - "Expected to reach the main screen with the Cash button after login" + "Expected to reach the main screen with the Wallet tab after login" ) let walletButton = app.buttons["Wallet"] @@ -24,9 +24,10 @@ final class LoginSmokeTests: BaseUITestCase { // Verify we're on the main screen assertMainScreenReached() - // Open Settings, go to My Account, and log out - waitAndTap(app.buttons["Settings"]) - waitAndTap(app.buttons["My Account"]) + // Settings lives on the You tab now: You → Advanced → Log Out. The + // settings rows sit under the tip card, so scroll them into view. + waitAndTap(app.buttons["You"]) + scrollUpToAndTap(app.buttons["Advanced"], in: app.scrollViews.firstMatch) waitAndTap(app.buttons["Log Out"]) // Confirmation dialog — scoped to the dialog container diff --git a/FlipcashUITests/Support/BaseUITestCase.swift b/FlipcashUITests/Support/BaseUITestCase.swift index a021a57c7..70381ad60 100644 --- a/FlipcashUITests/Support/BaseUITestCase.swift +++ b/FlipcashUITests/Support/BaseUITestCase.swift @@ -102,6 +102,56 @@ class BaseUITestCase: XCTestCase { element.tap() } + /// Waits for an element to exist, swipes `container` up until it is hittable, + /// and taps it. + /// + /// Use for content that is in the hierarchy but below the fold — an + /// off-screen element is never hittable, so `waitUntilHittableAndTap` just + /// burns its whole timeout waiting for a scroll that nothing performs. + func scrollUpToAndTap( + _ element: XCUIElement, + in container: XCUIElement, + maxSwipes: Int = 8, + _ message: String? = nil + ) { + XCTAssertTrue( + element.waitForExistence(timeout: 30), + message ?? "Expected \(element) to exist within 30s" + ) + + // `.slow` keeps the scroll from flinging: a fling is still decelerating + // when the swipe call returns, so the element the query just found + // hittable has moved by the time the tap lands. + var swipes = 0 + while !element.isHittable, swipes < maxSwipes { + container.swipeUp(velocity: .slow) + swipes += 1 + } + + XCTAssertTrue( + element.isHittable, + message ?? "Expected \(element) to be hittable after \(maxSwipes) swipes" + ) + + waitForStableFrame(element) + element.tap() + } + + /// Blocks until `element`'s frame stops moving, so a tap can't land on + /// whatever slid into its place. Content arriving asynchronously (balances, + /// recent activity) reflows the page under a scroll that has already ended, + /// which a settled scroll offset alone wouldn't catch. + func waitForStableFrame(_ element: XCUIElement, timeout: TimeInterval = 5) { + let deadline = Date().addingTimeInterval(timeout) + var previous = element.frame + while Date() < deadline { + Thread.sleep(forTimeInterval: 0.2) + let current = element.frame + if current == previous { return } + previous = current + } + } + /// Asserts that the main screen has been reached by checking for the Wallet /// tab, the launch tab of the tab-bar UI. /// diff --git a/FlipcashUITests/Support/Screens/AddMoneyStartScreen.swift b/FlipcashUITests/Support/Screens/AddMoneyStartScreen.swift index 7b68b561b..e4264f1da 100644 --- a/FlipcashUITests/Support/Screens/AddMoneyStartScreen.swift +++ b/FlipcashUITests/Support/Screens/AddMoneyStartScreen.swift @@ -6,7 +6,7 @@ import XCTest /// Page object for the standalone Add Money flow: "No Balance Yet" → -/// "Select Method" → the per-method screens. +/// "Add Money With" → the per-method screens. @MainActor struct AddMoneyStartScreen { @@ -22,19 +22,23 @@ struct AddMoneyStartScreen { app.staticTexts["No Balance Yet"] } - /// The "No Balance Yet" primary CTA. + /// The "No Balance Yet" primary CTA, scoped to the dialog. An unscoped + /// "Add Money" match can resolve to the wallet behind the dialog, which has + /// both a tile and a new-user tutorial row under that label. var addMoneyButton: XCUIElement { - app.buttons["Add Money"].firstMatch + app.otherElements["No Balance Yet"].buttons["Add Money"] } - // MARK: - Select Method + // MARK: - Add Money With - var selectMethodTitle: XCUIElement { - app.staticTexts["Select Method"] + /// The method picker's heading. The tab-bar UI titles it "Add Money With"; + /// "Select Method" was the pre-tab-bar heading. + var methodPickerTitle: XCUIElement { + app.staticTexts["Add Money With"] } - /// Coinbase row, matched by identifier — the visible label is the U+F8FF - /// Apple glyph + "Pay", brittle to match by label predicate. + /// Debit card (Coinbase) row, matched by identifier — the row carries the + /// U+F8FF Apple glyph + "Pay" as its trailing icon, brittle to match by label. var payDebitCardButton: XCUIElement { app.buttons["apple-pay-method-button"] } @@ -57,18 +61,6 @@ struct AddMoneyStartScreen { app.buttons["Connect Your Phantom Wallet"] } - // MARK: - Amount to Add - - var amountToAddNavBar: XCUIElement { - app.navigationBars["Amount to Add"] - } - - /// The action button on the "Amount to Add" screen. The "No Balance Yet" - /// dialog is gone before this screen exists, so "Add Money" is unambiguous. - var amountToAddActionButton: XCUIElement { - app.buttons["Add Money"].firstMatch - } - // MARK: - Assertions func assertNoBalanceReached(timeout: TimeInterval = 10) { @@ -78,17 +70,10 @@ struct AddMoneyStartScreen { ) } - func assertSelectMethodReached(timeout: TimeInterval = 10) { - XCTAssertTrue( - selectMethodTitle.waitForExistence(timeout: timeout), - "Expected the 'Select Method' sheet" - ) - } - - func assertAmountToAddReached(timeout: TimeInterval = 10) { + func assertMethodPickerReached(timeout: TimeInterval = 10) { XCTAssertTrue( - amountToAddNavBar.waitForExistence(timeout: timeout), - "Expected the 'Amount to Add' screen" + methodPickerTitle.waitForExistence(timeout: timeout), + "Expected the 'Add Money With' sheet" ) } diff --git a/FlipcashUITests/Support/Screens/WalletScreen.swift b/FlipcashUITests/Support/Screens/WalletScreen.swift index 0334d8682..f15c7f003 100644 --- a/FlipcashUITests/Support/Screens/WalletScreen.swift +++ b/FlipcashUITests/Support/Screens/WalletScreen.swift @@ -5,8 +5,8 @@ import XCTest -/// Page object for the Wallet (BalanceScreen). -/// Lists owned currencies and USDF reserves. +/// Page object for the Wallet tab: the balance header, the deck of per-currency +/// bill cards, and the tile shortcuts below the recent activity. @MainActor struct WalletScreen { @@ -18,16 +18,19 @@ struct WalletScreen { // MARK: - Elements - /// The first non-USDF currency row. Identified by the row's accessibility - /// identifier rather than `app.cells`, since the wallet uses a `ScrollView` - /// + `LazyVStack` rather than a `List`. The USDF row carries the - /// distinct identifier "currency-row-usdf" so this selector reliably - /// targets an investable token regardless of where USDF sorts. + /// The wallet's scrolling content, the container the tiles are scrolled in. + var scrollView: XCUIElement { app.scrollViews.firstMatch } + + /// The first non-USDF currency card. Identified by the card's accessibility + /// identifier rather than `app.cells`, since the wallet is a `ScrollView` of + /// stacked cards rather than a `List`. The USDF card carries the distinct + /// identifier "currency-row-usdf" so this selector reliably targets an + /// investable token regardless of where USDF sorts. var firstCurrencyRow: XCUIElement { app.buttons.matching(identifier: "currency-row").firstMatch } - /// The USDF row in the wallet. Always present once balances have synced. + /// The USDF card in the wallet. Always present once balances have synced. var usdfRow: XCUIElement { app.buttons["currency-row-usdf"] } @@ -35,9 +38,14 @@ struct WalletScreen { /// The balance header button that shows the flag + total amount + chevron. var balanceHeader: XCUIElement { app.buttons["balance-header"] } + /// The "Add Money" tile. Matched by identifier, not label: the new-user + /// tutorial above it renders an "Add Money" row too, and on a funded account + /// that row is completed and therefore disabled. + var addMoneyTile: XCUIElement { app.buttons["wallet-tile-add-money"] } + // MARK: - Actions - /// Opens the Wallet sheet from the main screen and waits for it to load. + /// Opens the Wallet tab and waits for it to load. func open(from testCase: BaseUITestCase) { testCase.waitAndTap(app.buttons["Wallet"]) } @@ -47,7 +55,13 @@ struct WalletScreen { testCase.waitAndTap(balanceHeader) } - /// Selects the first currency row and verifies CurrencyInfoScreen is reached. + /// Scrolls the "Add Money" tile into view and taps it. The tiles sit below + /// the card deck and the recent activity, so they start off-screen. + func tapAddMoneyTile(from testCase: BaseUITestCase) { + testCase.scrollUpToAndTap(addMoneyTile, in: scrollView) + } + + /// Selects the first currency card and verifies CurrencyInfoScreen is reached. func selectFirstCurrency() { XCTAssertTrue( firstCurrencyRow.waitForExistence(timeout: 30),