Files
InvestmentTrackerApp/PortfolioJournalUITests/PortfolioJournalUITests.swift
T
alexandrev-tibco ff67ea1e71 Fix causa raíz de taps muertos en Charts iPad: AppBackground interceptaba toques (build 50)
El círculo decorativo de AppBackground (70% del ancho del panel, offset -35%)
se desborda de su panel por diseño y en layouts side-by-side queda flotando
SOBRE el sidebar de Charts. Las shapes de SwiftUI participan en hit-testing por
defecto y el panel derecho va después en el HStack → cada tap en la zona del
sidebar cubierta por el círculo moría en silencio. El patrón lo delató: fallaban
Overview + Analyze (arriba, bajo el círculo) y funcionaban Risk + Forecast
(abajo). Dependía de la geometría (orientación/tamaño), por eso no reproducía
en el simulador en portrait.

- AppBackground: .allowsHitTesting(false) — un fondo decorativo jamás debe
  interceptar toques (fix global: aplica también a Sources/Journal en iPad)
- Panel de detalle de Charts: .clipped() para que la decoración tampoco PINTE
  sobre el sidebar
- Selección premium nunca se bloquea: los charts premium se seleccionan y
  muestran teaser de desbloqueo en el área del chart (chart_locked_* ×7 idiomas);
  el paywall se presenta desde el botón (contexto fiable)
- UITests: testChartSidebarSelection ahora corre en landscape (geometría que
  reproducía el bug) + testChartSelectionWithoutPremium nuevo; --no-premium
  en ScreenshotMode para testear la experiencia free

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
2026-07-08 23:30:30 +02:00

353 lines
13 KiB
Swift

import XCTest
/// UI Tests for Portfolio Journal app
/// These tests verify the app's user interface and navigation flows
final class PortfolioJournalUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
override func tearDownWithError() throws {
app = nil
}
// MARK: - App Launch Tests
func testAppLaunches() throws {
// Verify the app launches successfully
XCTAssertTrue(app.exists)
}
func testTabBarExists() throws {
// Wait for the tab bar to appear
let tabBar = app.tabBars.firstMatch
let exists = tabBar.waitForExistence(timeout: 5)
XCTAssertTrue(exists, "Tab bar should exist after launch")
}
// MARK: - Navigation Tests
func testDashboardTabIsSelected() throws {
// Dashboard should be the default selected tab
let tabBar = app.tabBars.firstMatch
_ = tabBar.waitForExistence(timeout: 5)
// Look for Dashboard tab button
let dashboardTab = tabBar.buttons["Dashboard"]
if dashboardTab.exists {
XCTAssertTrue(dashboardTab.isSelected || dashboardTab.isHittable)
}
}
func testNavigateToSourcesTab() throws {
let tabBar = app.tabBars.firstMatch
_ = tabBar.waitForExistence(timeout: 5)
let sourcesTab = tabBar.buttons["Sources"]
if sourcesTab.exists {
sourcesTab.tap()
// Verify we're on the Sources screen
XCTAssertTrue(sourcesTab.isSelected || sourcesTab.isHittable)
}
}
func testNavigateToGoalsTab() throws {
let tabBar = app.tabBars.firstMatch
_ = tabBar.waitForExistence(timeout: 5)
let goalsTab = tabBar.buttons["Goals"]
if goalsTab.exists {
goalsTab.tap()
XCTAssertTrue(goalsTab.isSelected || goalsTab.isHittable)
}
}
func testNavigateToJournalTab() throws {
let tabBar = app.tabBars.firstMatch
_ = tabBar.waitForExistence(timeout: 5)
let journalTab = tabBar.buttons["Journal"]
if journalTab.exists {
journalTab.tap()
XCTAssertTrue(journalTab.isSelected || journalTab.isHittable)
}
}
func testNavigateToSettingsTab() throws {
let tabBar = app.tabBars.firstMatch
_ = tabBar.waitForExistence(timeout: 5)
let settingsTab = tabBar.buttons["Settings"]
if settingsTab.exists {
settingsTab.tap()
XCTAssertTrue(settingsTab.isSelected || settingsTab.isHittable)
}
}
// MARK: - All Tabs Navigation Test
func testNavigateThroughAllTabs() throws {
let tabBar = app.tabBars.firstMatch
guard tabBar.waitForExistence(timeout: 5) else {
XCTFail("Tab bar not found")
return
}
let tabs = ["Dashboard", "Sources", "Goals", "Journal", "Settings"]
for tabName in tabs {
let tab = tabBar.buttons[tabName]
if tab.exists && tab.isHittable {
tab.tap()
// Give time for navigation
Thread.sleep(forTimeInterval: 0.3)
}
}
// Return to Dashboard
let dashboardTab = tabBar.buttons["Dashboard"]
if dashboardTab.exists {
dashboardTab.tap()
}
}
// MARK: - App Store Screenshot Capture
/// Captures the redesigned iPad Charts screen (sidebar tiles + KPI header).
/// Works on both iPad (sidebar buttons) and iPhone (tab bar).
func testCaptureChartsScreen() throws {
let capture = XCUIApplication()
capture.launchArguments = ["--screenshots"]
capture.launch()
// Wait for main UI (tab bar on iPhone, sidebar on iPad)
Thread.sleep(forTimeInterval: 4.0)
// Dismiss the notification-permission system alert if it appears
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let denyButton = springboard.alerts.buttons["Don't Allow"]
if denyButton.waitForExistence(timeout: 3) {
denyButton.tap()
Thread.sleep(forTimeInterval: 1.0)
}
// Try tab bar first (iPhone), then any button labeled Charts (iPad sidebar)
let tabBar = capture.tabBars.firstMatch
if tabBar.exists && tabBar.buttons["Charts"].exists {
tabBar.buttons["Charts"].tap()
} else {
let chartsButton = capture.buttons["Charts"].firstMatch
if chartsButton.waitForExistence(timeout: 5) {
chartsButton.tap()
} else {
capture.staticTexts["Charts"].firstMatch.tap()
}
}
Thread.sleep(forTimeInterval: 3.0)
func snap(_ name: String) {
let shot = XCTAttachment(screenshot: capture.screenshot())
shot.name = name
shot.lifetime = .keepAlways
add(shot)
}
snap("01_Evolution_default")
// Switch period to All (exercises the shared snapshot cache across ranges)
let allSegment = capture.buttons["All"].firstMatch
if allSegment.waitForExistence(timeout: 3) {
allSegment.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("02_Evolution_All")
}
// Switch chart type to a free multi-control chart and back (cache reuse path)
let compareTile = capture.buttons["Compare"].firstMatch
if compareTile.waitForExistence(timeout: 3) {
compareTile.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("03_Compare")
}
let periodTile = capture.buttons["Period vs Period"].firstMatch
if periodTile.waitForExistence(timeout: 3) {
periodTile.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("04_PeriodVsPeriod")
}
let evolutionTile = capture.buttons["Evolution"].firstMatch
if evolutionTile.waitForExistence(timeout: 3) {
evolutionTile.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("05_Evolution_back")
}
}
/// Regression (1.4.2): selecting Allocation / Contributions / Rolling 12M in the
/// Charts sidebar must actually switch the chart taps on the old custom tiles
/// were unreliable on iPad. Also verifies the Rolling 12M period filter reacts.
func testChartSidebarSelection() throws {
// Landscape reproduces the decorative-background overlay geometry that
// killed sidebar taps (AppBackground circle overflowing its panel).
XCUIDevice.shared.orientation = .landscapeLeft
let capture = XCUIApplication()
capture.launchArguments = ["--screenshots"]
capture.launch()
Thread.sleep(forTimeInterval: 4.0)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let denyButton = springboard.alerts.buttons["Don't Allow"]
if denyButton.waitForExistence(timeout: 3) {
denyButton.tap()
Thread.sleep(forTimeInterval: 1.0)
}
let tabBar = capture.tabBars.firstMatch
if tabBar.exists && tabBar.buttons["Charts"].exists {
tabBar.buttons["Charts"].tap()
} else {
// iPad main sidebar rows are cells with a static text label
capture.staticTexts["Charts"].firstMatch.tap()
}
Thread.sleep(forTimeInterval: 2.0)
func snap(_ name: String) {
let shot = XCTAttachment(screenshot: capture.screenshot())
shot.name = name
shot.lifetime = .keepAlways
add(shot)
}
// Allocation
let allocationRow = capture.staticTexts["Allocation"].firstMatch
XCTAssertTrue(allocationRow.waitForExistence(timeout: 5), "Allocation row not found in sidebar")
allocationRow.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("charts_allocation")
XCTAssertTrue(
capture.staticTexts["Asset Allocation"].firstMatch.waitForExistence(timeout: 5),
"Allocation chart did not load after tapping its sidebar row"
)
// Contributions
let contributionsRow = capture.staticTexts["Contributions"].firstMatch
XCTAssertTrue(contributionsRow.waitForExistence(timeout: 5), "Contributions row not found")
contributionsRow.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("charts_contributions")
XCTAssertTrue(
capture.staticTexts["Monthly avg"].firstMatch.waitForExistence(timeout: 5),
"Contributions chart did not load after tapping its sidebar row"
)
// Rolling 12M + period filter
let rollingRow = capture.staticTexts["Rolling 12M"].firstMatch
XCTAssertTrue(rollingRow.waitForExistence(timeout: 5), "Rolling 12M row not found")
rollingRow.tap()
Thread.sleep(forTimeInterval: 2.0)
snap("charts_rolling_all")
let threeMonths = capture.buttons["3M"].firstMatch
if threeMonths.waitForExistence(timeout: 3) {
threeMonths.tap()
Thread.sleep(forTimeInterval: 1.5)
snap("charts_rolling_3m")
}
}
/// Regression (1.4.2): WITHOUT premium, tapping a premium chart must still
/// select it and show the unlock teaser blocking selection behind a paywall
/// sheet made premium tiles look completely dead on non-premium devices.
func testChartSelectionWithoutPremium() throws {
let capture = XCUIApplication()
capture.launchArguments = ["--screenshots", "--no-premium"]
capture.launch()
Thread.sleep(forTimeInterval: 4.0)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let denyButton = springboard.alerts.buttons["Don't Allow"]
if denyButton.waitForExistence(timeout: 3) {
denyButton.tap()
Thread.sleep(forTimeInterval: 1.0)
}
let tabBar = capture.tabBars.firstMatch
if tabBar.exists && tabBar.buttons["Charts"].exists {
tabBar.buttons["Charts"].tap()
} else {
capture.staticTexts["Charts"].firstMatch.tap()
}
Thread.sleep(forTimeInterval: 2.0)
// Premium chart: must select and show the unlock teaser (not a dead tap)
let allocationRow = capture.staticTexts["Allocation"].firstMatch
XCTAssertTrue(allocationRow.waitForExistence(timeout: 5), "Allocation row not found in sidebar")
allocationRow.tap()
Thread.sleep(forTimeInterval: 2.0)
let shot = XCTAttachment(screenshot: capture.screenshot())
shot.name = "charts_allocation_locked"
shot.lifetime = .keepAlways
add(shot)
XCTAssertTrue(
capture.buttons["Unlock Premium"].firstMatch.waitForExistence(timeout: 5),
"Premium chart tap did not select the chart / show the unlock teaser"
)
// The unlock button must present the paywall
capture.buttons["Unlock Premium"].firstMatch.tap()
Thread.sleep(forTimeInterval: 2.0)
let paywallShot = XCTAttachment(screenshot: capture.screenshot())
paywallShot.name = "charts_paywall"
paywallShot.lifetime = .keepAlways
add(paywallShot)
}
/// Captures full-screen screenshots of the main tabs with demo data for App Store
/// marketing. Launches the app in `--screenshots` mode (onboarding/lock skipped,
/// SampleDataService seeded). Language can be driven via the SCREENSHOT_LANG env var
/// (e.g. "es", "de"). Screenshots are attached to the test result and extracted from
/// the .xcresult afterwards by the framing pipeline.
func testCaptureScreenshots() throws {
let capture = XCUIApplication()
capture.launchArguments = ["--screenshots"]
if let lang = ProcessInfo.processInfo.environment["SCREENSHOT_LANG"], !lang.isEmpty {
capture.launchArguments += ["-AppleLanguages", "(\(lang))", "-AppleLocale", lang]
}
capture.launch()
let tabBar = capture.tabBars.firstMatch
guard tabBar.waitForExistence(timeout: 20) else {
XCTFail("Tab bar not found in screenshot mode")
return
}
// Let demo data seed and charts render.
Thread.sleep(forTimeInterval: 3.0)
func snap(_ name: String) {
let shot = XCTAttachment(screenshot: capture.screenshot())
shot.name = name
shot.lifetime = .keepAlways
add(shot)
}
// Dashboard is already selected on launch.
snap("01_Dashboard")
for (index, tabName) in ["Sources", "Goals", "Journal", "Settings"].enumerated() {
let tab = tabBar.buttons[tabName]
if tab.waitForExistence(timeout: 5), tab.isHittable {
tab.tap()
Thread.sleep(forTimeInterval: 1.5)
snap(String(format: "%02d_%@", index + 2, tabName))
}
}
}
}