From 171023a4117c2170bdfe1732336915683f515d62 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Wed, 16 Sep 2026 15:13:10 +0200 Subject: [PATCH] =?UTF-8?q?Tests:=20corrige=203=20tests=20desfasados=20y?= =?UTF-8?q?=20a=C3=B1ade=20recorrido=20UI=20del=20layout=20adaptativo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReviewPromptServiceTests: la regla vigente pide 2 check-ins, no 3. - MonthlyCheckInViewTests: el día 1 cae en el periodo de gracia (20 días); usar el día 25 para etiquetar febrero. - SettingsViewModelTests: isPremium llega por receive(on: main); dar una vuelta de run loop antes de actuar. - DuoLayoutUITests: recorre tabs, split views e inspector con capturas (ejecutar en iPhone 17 Pro y iPad mini). ScreenshotMode oculta los tips de TipKit; identificador de accesibilidad en el botón de Quick Update. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn --- .../App/PortfolioJournalApp.swift | 3 + .../Views/Dashboard/DashboardView.swift | 1 + .../Services/ReviewPromptServiceTests.swift | 2 +- .../ViewModels/SettingsViewModelTests.swift | 4 + .../Views/MonthlyCheckInViewTests.swift | 4 +- .../DuoLayoutUITests.swift | 96 +++++++++++++++++++ 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 PortfolioJournalUITests/DuoLayoutUITests.swift diff --git a/PortfolioJournal/App/PortfolioJournalApp.swift b/PortfolioJournal/App/PortfolioJournalApp.swift index e374d55..371efaf 100644 --- a/PortfolioJournal/App/PortfolioJournalApp.swift +++ b/PortfolioJournal/App/PortfolioJournalApp.swift @@ -24,6 +24,9 @@ enum ScreenshotMode { // the premium charts without a StoreKit purchase. `--no-premium` keeps it // off to test the free-tier experience (locked charts, teasers). d.set(!CommandLine.arguments.contains("--no-premium"), forKey: "debugPremiumOverride") + // TipKit popovers cover toolbar buttons and swallow the first tap in + // automated walkthroughs — keep them out of captures. + Tips.hideAllTipsForTesting() } /// Seed demo data (no-op if the store already has sources). Called once Core Data diff --git a/PortfolioJournal/Views/Dashboard/DashboardView.swift b/PortfolioJournal/Views/Dashboard/DashboardView.swift index 8c9ba9e..2d613d4 100644 --- a/PortfolioJournal/Views/Dashboard/DashboardView.swift +++ b/PortfolioJournal/Views/Dashboard/DashboardView.swift @@ -84,6 +84,7 @@ struct DashboardView: View { } label: { Image(systemName: "plus.circle.fill") } + .accessibilityIdentifier("dashboard.quickUpdate") .popoverTip(QuickUpdateTip()) } ToolbarItem(placement: .topBarTrailing) { diff --git a/PortfolioJournalTests/Services/ReviewPromptServiceTests.swift b/PortfolioJournalTests/Services/ReviewPromptServiceTests.swift index 7daca29..60e66c1 100644 --- a/PortfolioJournalTests/Services/ReviewPromptServiceTests.swift +++ b/PortfolioJournalTests/Services/ReviewPromptServiceTests.swift @@ -26,7 +26,7 @@ final class ReviewPromptServiceTests: XCTestCase { reviewRequestHandler: { didRequest = true } ) - service.recordMonthlyCheckInCompleted() + // Rating velocity rule (1.6.x): 2 check-ins are enough, not 3. service.recordMonthlyCheckInCompleted() XCTAssertFalse(didRequest) diff --git a/PortfolioJournalTests/ViewModels/SettingsViewModelTests.swift b/PortfolioJournalTests/ViewModels/SettingsViewModelTests.swift index 928cc24..0942077 100644 --- a/PortfolioJournalTests/ViewModels/SettingsViewModelTests.swift +++ b/PortfolioJournalTests/ViewModels/SettingsViewModelTests.swift @@ -32,6 +32,10 @@ final class SettingsViewModelTests: XCTestCase { iap.setDebugPremiumOverride(true) #endif let viewModel = SettingsViewModel(iapService: iap) + // isPremium is mirrored via `receive(on: DispatchQueue.main)`; give the + // publisher a run-loop turn before acting on it. + RunLoop.main.run(until: Date().addingTimeInterval(0.2)) + XCTAssertTrue(viewModel.isPremium) viewModel.setBackupsEnabled(true) diff --git a/PortfolioJournalTests/Views/MonthlyCheckInViewTests.swift b/PortfolioJournalTests/Views/MonthlyCheckInViewTests.swift index 5df0496..bc207e1 100644 --- a/PortfolioJournalTests/Views/MonthlyCheckInViewTests.swift +++ b/PortfolioJournalTests/Views/MonthlyCheckInViewTests.swift @@ -6,7 +6,9 @@ final class MonthlyCheckInViewTests: XCTestCase { var components = DateComponents() components.year = 2026 components.month = 2 - components.day = 1 + // Past the 20-day grace period (MonthlyCheckInStore.graceDays), so the + // effective month is February itself. + components.day = 25 let date = Calendar(identifier: .gregorian).date(from: components)! let label = MonthlyCheckInView.monthLabel( diff --git a/PortfolioJournalUITests/DuoLayoutUITests.swift b/PortfolioJournalUITests/DuoLayoutUITests.swift new file mode 100644 index 0000000..cf566dc --- /dev/null +++ b/PortfolioJournalUITests/DuoLayoutUITests.swift @@ -0,0 +1,96 @@ +import XCTest + +/// Walks the adaptive layout (tab bar / sidebar, split views, inspector) and +/// attaches a screenshot per step. Run on a compact device (iPhone) and on a +/// regular×regular one (iPad mini ≈ iPhone Duo inner screen) to compare. +final class DuoLayoutUITests: XCTestCase { + + override func setUpWithError() throws { + continueAfterFailure = true + } + + private func snap(_ app: XCUIApplication, _ name: String) { + let shot = XCTAttachment(screenshot: app.screenshot()) + shot.name = name + shot.lifetime = .keepAlways + add(shot) + } + + /// Tab bar button on iPhone, top tab bar / sidebar row on iPad. + private func openTab(_ app: XCUIApplication, _ title: String) { + let tabBar = app.tabBars.firstMatch + if tabBar.exists && tabBar.buttons[title].exists && tabBar.buttons[title].isHittable { + tabBar.buttons[title].tap() + } else if app.buttons[title].firstMatch.waitForExistence(timeout: 3) { + app.buttons[title].firstMatch.tap() + } else if app.staticTexts[title].firstMatch.waitForExistence(timeout: 3) { + app.staticTexts[title].firstMatch.tap() + } + Thread.sleep(forTimeInterval: 1.5) + } + + func testAdaptiveLayoutWalkthrough() throws { + let app = XCUIApplication() + app.launchArguments = ["--screenshots"] + app.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: 8) { + denyButton.tap() + Thread.sleep(forTimeInterval: 1.0) + } + + snap(app, "01_dashboard") + + // Quick Update: inspector column in regular width, sheet in compact. + let quickUpdate = app.buttons["dashboard.quickUpdate"].firstMatch + if quickUpdate.waitForExistence(timeout: 3) { + quickUpdate.tap() + Thread.sleep(forTimeInterval: 2.0) + snap(app, "02_quick_update_inspector") + let cancel = app.buttons["Cancel"].firstMatch + XCTAssertTrue(cancel.waitForExistence(timeout: 3), "Quick Update cancel button missing") + cancel.tap() + Thread.sleep(forTimeInterval: 1.5) + XCTAssertFalse(app.buttons["Cancel"].firstMatch.exists, "Quick Update did not dismiss") + snap(app, "03_dashboard_after_cancel") + } else { + XCTFail("Quick Update button not found") + } + + openTab(app, "Sources") + snap(app, "04_sources") + let sourceCells = app.cells + if sourceCells.count > 2 { + sourceCells.element(boundBy: 2).tap() + Thread.sleep(forTimeInterval: 2.0) + snap(app, "05_source_detail") + } + + openTab(app, "Charts") + snap(app, "06_charts") + + openTab(app, "Journal") + snap(app, "07_journal") + let journalCells = app.cells + if journalCells.count > 1 { + journalCells.element(boundBy: 1).tap() + Thread.sleep(forTimeInterval: 2.0) + snap(app, "08_journal_detail") + } + + openTab(app, "Settings") + snap(app, "09_settings") + + // Landscape: the Duo inner screen is regular×regular in both orientations. + XCUIDevice.shared.orientation = .landscapeLeft + Thread.sleep(forTimeInterval: 2.0) + openTab(app, "Home") + snap(app, "10_dashboard_landscape") + openTab(app, "Charts") + snap(app, "11_charts_landscape") + XCUIDevice.shared.orientation = .portrait + } +}