[coverage-85] Add HomeFiltersFlowTests, fix stuck activeModal on interactive sheet dismiss

FiltersModalView.swift was 0% covered. Reaching it via guest mode hit
the backend's App Attest simulator-bypass rejection (403
APP_ATTEST_VERIFICATION_FAILED on /api/public/session, confirmed via
curl - a pre-existing, out-of-repo backend issue already documented in
decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md), which
kept the guest address picker stuck showing a load-error sub-screen.

While chasing that, found a real bug in ContentView.swift:
.sheet(item: $appState.activeModal) had no onDismiss, so an
interactive swipe-to-dismiss never reset the bound item to nil -
anyone who swipes the address picker away without picking a location
leaves appState.activeModal stuck non-nil, silently breaking the next
modal presentation app-wide. Fixed with
onDismiss: { appState.activeModal = nil }.

Rewrote HomeFiltersFlowTests to use the authenticated QA account
instead of guest mode, routing around the backend-blocked guest wall
entirely. Also fixed a separate, real XCUITest issue: a plain .tap()
on the filter icon (inside Home's .offset()-transformed collapsing
header) reliably produced "Computed hit point {-1, -1}" - fixed via
coordinate(withNormalizedOffset:).tap().

Verified: HomeFiltersFlowTests passed clean across 2 consecutive
isolated runs. Full-suite regression run could not be completed this
round - the local environment stopped completing any instrumented
test launch afterward (reproduced across background/foreground,
clean DerivedData, simulator reboot, decoupled build/test, process
cleanup), unrelated to these changes. Documented in
decisions/2026-08-11-coverage-push-to-85-percent-status.md, with a
next-session TODO to run the full suite once the environment recovers.
This commit is contained in:
Daniel Arantes Loverde
2026-08-12 22:41:26 -03:00
parent b68cd92307
commit 2a934483fd
3 changed files with 73 additions and 1 deletions

View File

@@ -92,6 +92,7 @@
9EDDCECCAEEB8FA4909DA9D4 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBFCE20C54A79DEA8F4512B /* AppCoordinator.swift */; };
9F09502CF9331F0C94BFA5F0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B2C5BBDFF4473C472F45B4 /* AppState.swift */; };
A115420D6854B7C9DCF339DE /* CartStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9CDFF62AE19F3EE36A1D19 /* CartStateTests.swift */; };
A24EE365C06DBA8FD2D91213 /* HomeFiltersFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44E3CD64AF31CEFFD932D7D /* HomeFiltersFlowTests.swift */; };
A3DFC65C635BE5D96660A432 /* Inputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB95D6804877D2ED244811D0 /* Inputs.swift */; };
A68ED6FBE5307F8674A5A4AB /* HomeView+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CCEF22D356F9EC723999D /* HomeView+Data.swift */; };
A6AE21C3CC220D50251865BE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DCCE423400D4D3785EE0F0 /* Assets.xcassets */; };
@@ -270,6 +271,7 @@
B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackbarCenterTests.swift; sourceTree = "<group>"; };
B33118037CF56CD70C332E2C /* UITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestSupport.swift; sourceTree = "<group>"; };
B35D1C07050150E305FAF09A /* PizzaProductDetailSheet+Flow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PizzaProductDetailSheet+Flow.swift"; sourceTree = "<group>"; };
B44E3CD64AF31CEFFD932D7D /* HomeFiltersFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFiltersFlowTests.swift; sourceTree = "<group>"; };
B807C1B779CAE1340D377D43 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
B85C265A23C9BA93E3DD6898 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
BBD89D11DF567A6A6E078CC8 /* HomeGuestFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeGuestFlowTests.swift; sourceTree = "<group>"; };
@@ -516,6 +518,7 @@
C3A0787FEAE67318791D6CFD /* AuthenticatedProfileNavigationTests.swift */,
BEAA5F0BA8EBC7F2D19472D6 /* AuthenticatedSessionFlowTests.swift */,
7034E89267D9F5D86BF1E424 /* CartCheckoutFlowTests.swift */,
B44E3CD64AF31CEFFD932D7D /* HomeFiltersFlowTests.swift */,
BBD89D11DF567A6A6E078CC8 /* HomeGuestFlowTests.swift */,
FE0C7A506EDE8D0FD8F16AC6 /* OrderDetailsFlowTests.swift */,
56E3E6C1834786CA3621B7BB /* PediFoodsUITestsLaunchTests.swift */,
@@ -738,6 +741,7 @@
09FA9556CD659F93C25447DB /* AuthenticatedProfileNavigationTests.swift in Sources */,
1AFC280D183E5DC53949C317 /* AuthenticatedSessionFlowTests.swift in Sources */,
4B85C214A0BF3DF50311C76C /* CartCheckoutFlowTests.swift in Sources */,
A24EE365C06DBA8FD2D91213 /* HomeFiltersFlowTests.swift in Sources */,
D7B6082157DA0DF2382EB943 /* HomeGuestFlowTests.swift in Sources */,
E3C2AE0D45A31B955EFAEEAD /* OrderDetailsFlowTests.swift in Sources */,
80B476CCE2B90BEE2FA4CB37 /* PediFoodsUITestsLaunchTests.swift in Sources */,

View File

@@ -52,7 +52,7 @@ struct ContentView: View {
.transition(.opacity)
}
}
.sheet(item: $appState.activeModal) { modal in
.sheet(item: $appState.activeModal, onDismiss: { appState.activeModal = nil }) { modal in
switch modal {
case .addressPicker:
AddressPickerModalView(appState: $appState, selectedTab: $selectedTab)

View File

@@ -0,0 +1,68 @@
import XCTest
/// Covers Home's filter sheet (FiltersModalView.swift, ~700 lines, was
/// 0% covered), reachable via the search bar's filter icon
/// (identifier "slider.horizontal.3"). Uses the authenticated QA account
/// rather than guest mode - the guest address picker
/// (PublicLocationPickerView) currently can't load its states/cities list
/// at all, because the backend rejects the simulator's App Attest bypass
/// with 403 APP_ATTEST_VERIFICATION_FAILED (confirmed directly via curl
/// against /api/public/session, a pre-existing backend-side issue, see
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md). That
/// leaves the guest address-picker sheet re-presenting itself indefinitely
/// (HomeView+Data.loadGuestStores() keeps re-triggering it while no
/// location is set), permanently covering Home. Logging in with the
/// standing QA account swaps AddressPickerModalView to the
/// App-Attest-free AddressesView instead, routing around the guest wall
/// entirely.
final class HomeFiltersFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testFiltersSheetSelectionsAndApply() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
// ensureLoggedIn can land on whichever tab it detected the
// authenticated session from - switch explicitly.
XCTAssertTrue(app.buttons["Home"].waitForExistence(timeout: 10))
app.buttons["Home"].tap()
// The filter icon lives inside Home's collapsing header
// (SearchBar, positioned with .offset(y: collapseProgress * -120))
// - a plain .tap() was separately confirmed (while still chasing
// the guest-mode blocker above) to sometimes resolve to an invalid
// "Computed hit point {-1, -1}" for this transform-positioned
// element. Tapping a normalized-offset coordinate on the element
// bypasses XCUITest's automatic hit-point computation.
let filterIcon = app.buttons.matching(identifier: "slider.horizontal.3").firstMatch
XCTAssertTrue(filterIcon.waitForExistence(timeout: 10), "Filter icon never appeared on Home")
filterIcon.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
XCTAssertTrue(app.staticTexts["Filtros"].waitForExistence(timeout: 5), "Never reached the filters sheet")
// Sort option - just needs to be selectable, no assertion on the
// resulting Home state (that's real backend store data, out of
// scope here).
let ratingSort = app.buttons["Avaliação"]
if ratingSort.exists {
ratingSort.tap()
}
// Price tier - label is the raw "$"/"$$"/etc symbol.
let mediumPrice = app.buttons["$$"]
if mediumPrice.exists {
mediumPrice.tap()
}
app.buttons["Aplicar Filtros"].tap()
// Applying dismisses back to Home.
XCTAssertTrue(app.textFields["Search menu, restaurant or craving"].waitForExistence(timeout: 10), "Never returned to Home after applying filters")
}
}