db844d5e80
Charts iPad (rediseño): - Fila de KPIs sobre el chart: Total Value, Period Return, CAGR, Volatility y Max Drawdown del rango activo (nuevo ChartsViewModel.portfolioMetrics, calculado sobre los TOTALES MENSUALES AGREGADOS — pasar snapshots multi-source crudos a calculateMetrics producía KPIs absurdos) - Sidebar (248pt) con tiles de chart type en grid 2 col agrupados por sección: Overview / Analyze / Risk / Forecast, con candado premium y accesibilidad - Selector de periodo como Picker segmentado nativo en la cabecera del chart (fuera del sidebar); título + descripción del chart visibles - Eliminado sheet de paywall duplicado del layout iPad Zoom (todas las series temporales): - Nuevo ChartZoom.swift: pinch + botones +/- y reset (HIG: el gesto nunca es el único camino), chartScrollableAxes + chartXVisibleDomain al hacer zoom - Integrado en Evolution, Contributions, Rolling 12M, Cashflow, Drawdown y Volatility Focus Mode eliminado (todo siempre disponible): - Fuera el toggle de Dashboard/Charts/Settings/Onboarding y los 6 @AppStorage - Todos los chart types siempre visibles; variantes completas en SourceDetail/SourceList - Home: Total Portfolio Value muestra SIEMPRE el cambio desde el último check-in - PeriodReturnsCard siempre visible Rendimiento y bugs (de la auditoría): - El cache de snapshots ya no se invalida al cambiar solo de chart type/rango/breakdown - calculateAnnualizedGrowth y el forecast a 12 meses ahora componen (la aproximación lineal sobreestimaba con historiales cortos) - Drawdown sin force unwrap; simulador: rama muerta reemplazada por preservación real de las asignaciones simuladas del usuario - UITest de captura de Charts con manejo del alert de notificaciones 12 strings nuevas localizadas en los 7 idiomas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
201 lines
6.6 KiB
Swift
201 lines
6.6 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)
|
|
|
|
let shot = XCTAttachment(screenshot: capture.screenshot())
|
|
shot.name = "Charts_Redesign"
|
|
shot.lifetime = .keepAlways
|
|
add(shot)
|
|
}
|
|
|
|
/// 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))
|
|
}
|
|
}
|
|
}
|
|
}
|