From e86d16c9d2374d2d339eee49e14167ffe6d416a9 Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Thu, 10 Sep 2026 20:31:43 -0300 Subject: [PATCH 01/17] [ci] Add ASC secrets presence check for debugging auth failure --- .gitea/workflows/beta.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitea/workflows/beta.yml b/.gitea/workflows/beta.yml index cfe60c2..5f52542 100644 --- a/.gitea/workflows/beta.yml +++ b/.gitea/workflows/beta.yml @@ -20,6 +20,18 @@ jobs: "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${{ github.sha }}.tar.gz" tar -xzf /tmp/repo.tar.gz --strip-components=1 -C "$GITHUB_WORKSPACE" + - name: Check ASC secrets are present (no values printed) + run: | + for s in ASC_KEY_ID ASC_ISSUER_ID ASC_KEY_CONTENT CI_KEYCHAIN_PASSWORD; do + eval "v=\${${s}:-}" + if [ -n "$v" ]; then echo "$s: SET (${#v} chars)"; else echo "$s: EMPTY"; fi + done + env: + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }} + CI_KEYCHAIN_PASSWORD: ${{ secrets.CI_KEYCHAIN_PASSWORD }} + - name: Write App Store Connect API key run: | cat > fastlane/apikey.json < Date: Fri, 11 Sep 2026 08:54:01 -0300 Subject: [PATCH 02/17] Update project.pbxproj --- PediFoods.xcodeproj/project.pbxproj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PediFoods.xcodeproj/project.pbxproj b/PediFoods.xcodeproj/project.pbxproj index 2cad3cb..c2686b8 100644 --- a/PediFoods.xcodeproj/project.pbxproj +++ b/PediFoods.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 29E376FB24A2F19361D5BB6A /* ApiCustomerPayloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246FDD4C8985BF2F5C900001 /* ApiCustomerPayloadModels.swift */; }; 2A1940BB0EDC3FF50127B39A /* SnackbarOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4745C12F14B695C77DFE178 /* SnackbarOverlay.swift */; }; 2B202626C34FC9F5C87C0388 /* ApiOrderModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4625B70E3E8BEEB9D0B2042 /* ApiOrderModels.swift */; }; - 2F56755488E951C453AC2126 /* LCEssentials in Frameworks */ = {isa = PBXBuildFile; productRef = B8D5E8821C95A331C3A19DA1 /* LCEssentials */; }; 2FCB3403A764AD74AC3BD47C /* LegalDocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */; }; 33AB0B87867400F3D0C65FA0 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DBC4B9B43C015C4DD411359 /* ApiClient.swift */; }; 3689239282AADF06783CBBEA /* AuthFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B3C375FC18002A884A44F1 /* AuthFormatters.swift */; }; @@ -97,6 +96,7 @@ 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 */; }; + A8DD2855305422C50014CFA9 /* LCEssentials in Frameworks */ = {isa = PBXBuildFile; productRef = A8DD2854305422C50014CFA9 /* LCEssentials */; }; AA0A4F088033729F9B74B2BB /* LegalViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B6E69309F3EDF28E81CC /* LegalViews.swift */; }; ABE383C64BD045B9732AA8C5 /* HomeView+Filtering.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D7ECA55B1FE5FC617E84CB /* HomeView+Filtering.swift */; }; AC7E0F52FEF7976D97190205 /* ApiModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60DC2520DBC74D68AEE659E5 /* ApiModels.swift */; }; @@ -318,7 +318,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F56755488E951C453AC2126 /* LCEssentials in Frameworks */, + A8DD2855305422C50014CFA9 /* LCEssentials in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -659,7 +659,7 @@ ); name = PediFoods; packageProductDependencies = ( - B8D5E8821C95A331C3A19DA1 /* LCEssentials */, + A8DD2854305422C50014CFA9 /* LCEssentials */, ); productName = PediFoods; productReference = 7A6D74FD2D2F2F79349EA04E /* PediFoods.app */; @@ -700,7 +700,7 @@ ); mainGroup = C0C1E2AAD06BC8D9F221FFD5; packageReferences = ( - A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */, + A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */, ); projectDirPath = ""; projectRoot = ""; @@ -1314,20 +1314,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */ = { + A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git"; + repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.11; + minimumVersion = 2.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B8D5E8821C95A331C3A19DA1 /* LCEssentials */ = { + A8DD2854305422C50014CFA9 /* LCEssentials */ = { isa = XCSwiftPackageProductDependency; - package = A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */; + package = A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */; productName = LCEssentials; }; /* End XCSwiftPackageProductDependency section */ From a5827f9136fdd5241dfaaf7eec8404c59533a69c Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 09:02:17 -0300 Subject: [PATCH 03/17] fix: update performLCERequest for LCEssentials 2.0.2 API.request signature LCEssentials 2.0.2 replaced request(url:params:method:headers:jsonEncoding:debug:) with request(url:method:body:pathParams:headers:debug:timeoutInterval:networkServiceType:persistConnection:). The pre-encoded params Data is now wrapped in RawBody to preserve the same application/json Content-Type behavior that jsonEncoding: true used to set. --- PediFoods/Services/ApiClient.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PediFoods/Services/ApiClient.swift b/PediFoods/Services/ApiClient.swift index cfc38c9..7e0c577 100644 --- a/PediFoods/Services/ApiClient.swift +++ b/PediFoods/Services/ApiClient.swift @@ -175,12 +175,12 @@ extension ApiClient { headers: [String: String] ) async throws -> String { let httpMethod = toHTTPMethod(method) + let body: (any HTTPBody)? = params.map { RawBody(data: $0, contentType: "application/json; charset=UTF-8") } return try await API.shared.request( url: url, - params: params, method: httpMethod, + body: body, headers: headers, - jsonEncoding: true, debug: true ) } From 240cb288bf41561e3ab4463291c6fa9869cb828b Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 09:18:46 -0300 Subject: [PATCH 04/17] ci: prune unavailable simulators before running tests Shared macOS runner accumulates orphaned simulator entries (registry present, data dir missing on disk) which fail run_tests with 'Unable to boot device because it cannot be located on disk'. Clean up before every test run instead of requiring manual simctl intervention. --- .gitea/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 8e34e22..f0e04d0 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -19,5 +19,8 @@ jobs: "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${{ github.sha }}.tar.gz" tar -xzf /tmp/repo.tar.gz --strip-components=1 -C "$GITHUB_WORKSPACE" + - name: Prune orphaned simulators (shared runner, avoids stale-UDID boot failures) + run: xcrun simctl delete unavailable || true + - name: Run tests with coverage run: fastlane tests From 0fb948c9f603c4cf0b684329342bcd52af93c96b Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 09:45:26 -0300 Subject: [PATCH 05/17] fix(uitests): stop logging out after every authenticated test All 7 authenticated-flow XCTestCase classes logged out for real in tearDown, forcing a real OTP round trip against the production backend on the next authenticated test. Under CI's back-to-back suite run this produced ~9 real logins to the same QA phone number in ~9 minutes - the first few succeeded (~30-40s each) but every one after that failed to complete within timeout, cascading into 'Login never completed' across CartCheckoutFlowTests, HomeFiltersFlowTests, OrderDetailsFlowTests, SavedCardsFlowTests, UserProfileFlowTests. Removed the per-test logout so the QA account's Keychain-backed session persists across the whole suite (ensureLoggedIn already no-ops when already authenticated). Moved the 'must start logged out' guarantee to the two classes that actually need it - HomeGuestFlowTests and ProfileLoggedOutFlowTests now force logout themselves right after app.launch() via a small launch helper, instead of relying on whichever authenticated class happened to run last. See decisions/2026-09-11-ui-test-shared-login-session.md. --- .../AuthenticatedProfileNavigationTests.swift | 10 ++++++--- .../AuthenticatedSessionFlowTests.swift | 9 +++----- PediFoodsUITests/CartCheckoutFlowTests.swift | 4 +--- PediFoodsUITests/HomeFiltersFlowTests.swift | 4 +--- PediFoodsUITests/HomeGuestFlowTests.swift | 21 ++++++++++++------- PediFoodsUITests/OrderDetailsFlowTests.swift | 4 +--- .../ProfileLoggedOutFlowTests.swift | 18 +++++++++++----- PediFoodsUITests/SavedCardsFlowTests.swift | 4 +--- PediFoodsUITests/UserProfileFlowTests.swift | 4 +--- 9 files changed, 42 insertions(+), 36 deletions(-) diff --git a/PediFoodsUITests/AuthenticatedProfileNavigationTests.swift b/PediFoodsUITests/AuthenticatedProfileNavigationTests.swift index 631c48b..7079ee5 100644 --- a/PediFoodsUITests/AuthenticatedProfileNavigationTests.swift +++ b/PediFoodsUITests/AuthenticatedProfileNavigationTests.swift @@ -13,9 +13,13 @@ final class AuthenticatedProfileNavigationTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout: the standing QA account's Keychain session is + // meant to persist across authenticated-flow tests, so only the first + // one in a suite run pays for a real OTP round trip. Tests that need a + // guaranteed logged-out state (HomeGuestFlowTests, + // ProfileLoggedOutFlowTests) force it themselves in their own setUp + // instead of relying on every authenticated class to log out after + // itself - see decisions/2026-09-11-ui-test-shared-login-session.md. func testOrdersScreenReachableAndLoadsRealData() throws { let app = XCUIApplication() diff --git a/PediFoodsUITests/AuthenticatedSessionFlowTests.swift b/PediFoodsUITests/AuthenticatedSessionFlowTests.swift index 461d007..e3d3c3d 100644 --- a/PediFoodsUITests/AuthenticatedSessionFlowTests.swift +++ b/PediFoodsUITests/AuthenticatedSessionFlowTests.swift @@ -2,17 +2,14 @@ import XCTest /// Covers a real, authenticated session with the standing QA account (see /// TestFixtures.swift / decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md). -/// Logs out in tearDown so guest-only tests elsewhere in the same run don't -/// inherit an authenticated Keychain session. +/// No per-test logout - the Keychain session is meant to persist across +/// authenticated-flow tests in the same run; see +/// decisions/2026-09-11-ui-test-shared-login-session.md. final class AuthenticatedSessionFlowTests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } - func testLoginReachesAuthenticatedProfile() throws { let app = XCUIApplication() app.launch() diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index 0a1fae2..77efe88 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -10,9 +10,7 @@ final class CartCheckoutFlowTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testAddProductToCartAndReachCheckout() throws { let app = XCUIApplication() diff --git a/PediFoodsUITests/HomeFiltersFlowTests.swift b/PediFoodsUITests/HomeFiltersFlowTests.swift index b936d6e..bb6d67b 100644 --- a/PediFoodsUITests/HomeFiltersFlowTests.swift +++ b/PediFoodsUITests/HomeFiltersFlowTests.swift @@ -20,9 +20,7 @@ final class HomeFiltersFlowTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testFiltersSheetSelectionsAndApply() throws { let app = XCUIApplication() diff --git a/PediFoodsUITests/HomeGuestFlowTests.swift b/PediFoodsUITests/HomeGuestFlowTests.swift index 74a6a09..34a61be 100644 --- a/PediFoodsUITests/HomeGuestFlowTests.swift +++ b/PediFoodsUITests/HomeGuestFlowTests.swift @@ -10,9 +10,19 @@ final class HomeGuestFlowTests: XCTestCase { continueAfterFailure = false } - func testHomeRendersSearchAndGreeting() throws { + /// Authenticated-flow tests no longer log out after themselves (see + /// decisions/2026-09-11-ui-test-shared-login-session.md), so guest + /// tests must force a clean logged-out state themselves right after + /// launch, rather than assume one. + private func launchGuestApp() -> XCUIApplication { let app = XCUIApplication() app.launch() + app.logoutIfAuthenticated() + return app + } + + func testHomeRendersSearchAndGreeting() throws { + let app = launchGuestApp() XCTAssertTrue(app.reachGuestHome(), "Never reached Home's search field") XCTAssertTrue(app.staticTexts["ENTREGAR EM:"].exists) @@ -21,8 +31,7 @@ final class HomeGuestFlowTests: XCTestCase { } func testHomeShowsDefaultCategoryChip() throws { - let app = XCUIApplication() - app.launch() + let app = launchGuestApp() XCTAssertTrue(app.reachGuestHome()) XCTAssertTrue(app.staticTexts["Categories"].exists) @@ -30,8 +39,7 @@ final class HomeGuestFlowTests: XCTestCase { } func testHomeShowsGuestLocationPromptWhenNoLocationChosen() throws { - let app = XCUIApplication() - app.launch() + let app = launchGuestApp() XCTAssertTrue(app.reachGuestHome()) // No guest state/city selected -> Home's own inline message, not @@ -40,8 +48,7 @@ final class HomeGuestFlowTests: XCTestCase { } func testTappingAddressChipReopensLocationPicker() throws { - let app = XCUIApplication() - app.launch() + let app = launchGuestApp() XCTAssertTrue(app.reachGuestHome()) // The "ENTREGAR EM:" address button re-opens the same address diff --git a/PediFoodsUITests/OrderDetailsFlowTests.swift b/PediFoodsUITests/OrderDetailsFlowTests.swift index 74f8512..5a3a987 100644 --- a/PediFoodsUITests/OrderDetailsFlowTests.swift +++ b/PediFoodsUITests/OrderDetailsFlowTests.swift @@ -9,9 +9,7 @@ final class OrderDetailsFlowTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testOrderDetailsReachableFromOrdersList() throws { let app = XCUIApplication() diff --git a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift index 5a547e3..068bfa6 100644 --- a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift +++ b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift @@ -7,9 +7,19 @@ final class ProfileLoggedOutFlowTests: XCTestCase { continueAfterFailure = false } - func testLoggedOutProfileShowsLoginPrompt() throws { + /// Authenticated-flow tests no longer log out after themselves (see + /// decisions/2026-09-11-ui-test-shared-login-session.md), so + /// logged-out tests must force a clean state themselves right after + /// launch, rather than assume one. + private func launchLoggedOutApp() -> XCUIApplication { let app = XCUIApplication() app.launch() + app.logoutIfAuthenticated() + return app + } + + func testLoggedOutProfileShowsLoginPrompt() throws { + let app = launchLoggedOutApp() XCTAssertTrue(app.reachLoggedOutProfile(), "Never reached the logged-out profile screen") XCTAssertTrue(app.staticTexts["Entre na sua conta"].exists) @@ -22,8 +32,7 @@ final class ProfileLoggedOutFlowTests: XCTestCase { /// a working way back out (its `LCENavigationView` back button, which /// sets `root = .main`). func testAuthIntroScreenShowsChoicesAndCanGoBack() throws { - let app = XCUIApplication() - app.launch() + let app = launchLoggedOutApp() XCTAssertTrue(app.reachLoggedOutProfile()) app.buttons["Entrar ou Cadastrar"].tap() @@ -45,8 +54,7 @@ final class ProfileLoggedOutFlowTests: XCTestCase { /// app's standard `AppBackButtonIcon` via `LCENavigationView`, not the /// oversized iOS 26 system glass back button. func testPushedAuthScreensUseAppBackButton() throws { - let app = XCUIApplication() - app.launch() + let app = launchLoggedOutApp() XCTAssertTrue(app.reachLoggedOutProfile()) app.buttons["Entrar ou Cadastrar"].tap() diff --git a/PediFoodsUITests/SavedCardsFlowTests.swift b/PediFoodsUITests/SavedCardsFlowTests.swift index 3dd2b0f..a1f3949 100644 --- a/PediFoodsUITests/SavedCardsFlowTests.swift +++ b/PediFoodsUITests/SavedCardsFlowTests.swift @@ -9,9 +9,7 @@ final class SavedCardsFlowTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testAddCardFormReachableFromSavedCards() throws { let app = XCUIApplication() diff --git a/PediFoodsUITests/UserProfileFlowTests.swift b/PediFoodsUITests/UserProfileFlowTests.swift index 126afa6..8ad3eaa 100644 --- a/PediFoodsUITests/UserProfileFlowTests.swift +++ b/PediFoodsUITests/UserProfileFlowTests.swift @@ -7,9 +7,7 @@ final class UserProfileFlowTests: XCTestCase { continueAfterFailure = false } - override func tearDownWithError() throws { - XCUIApplication().logoutIfAuthenticated() - } + // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testUserProfileReachableFromProfileHeader() throws { let app = XCUIApplication() From 84be24f274aac768ad8518f384ff508c2843460b Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:00:51 -0300 Subject: [PATCH 06/17] fix(uitests): move forced logout to class setUp, fix ambiguous Ver Detalhes match Previous commit's per-test logoutIfAuthenticated() in HomeGuestFlowTests and ProfileLoggedOutFlowTests ran the guest address-picker navigation dance (reachProfileTabRegardlessOfAuthState) twice back-to-back per test - once in the new launch helper, once again inside reachGuestHome/ reachLoggedOutProfile - doubling exposure to that dance's known race and regressing 5 previously-stable tests ('Login never completed' was fixed, but new failures appeared in its place). Moved the forced logout into override class func setUp() so it runs once per class instead of once per test; test bodies are back to the plain XCUIApplication()+launch() pattern. Also fixed OrderDetailsFlowTests: 'Ver Detalhes' now matches multiple elements once the QA account has more than one real order (previously masked because login itself was failing first). Switched to a label predicate + .firstMatch. See decisions/2026-09-11-ui-test-shared-login-session.md follow-ups. --- PediFoodsUITests/HomeGuestFlowTests.swift | 39 ++++++++++++------- PediFoodsUITests/OrderDetailsFlowTests.swift | 10 ++++- .../ProfileLoggedOutFlowTests.swift | 37 +++++++++++------- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/PediFoodsUITests/HomeGuestFlowTests.swift b/PediFoodsUITests/HomeGuestFlowTests.swift index 34a61be..fd8b24d 100644 --- a/PediFoodsUITests/HomeGuestFlowTests.swift +++ b/PediFoodsUITests/HomeGuestFlowTests.swift @@ -6,23 +6,31 @@ import XCTest /// blocked by a separate login-navigation issue documented there and are /// not covered here. final class HomeGuestFlowTests: XCTestCase { + /// Authenticated-flow tests no longer log out after themselves (see + /// decisions/2026-09-11-ui-test-shared-login-session.md), so this + /// class must force a clean logged-out state before its tests run, + /// rather than assume one. Done once per class, not once per test: + /// the underlying navigate-to-Profile-tab dance + /// (`reachProfileTabRegardlessOfAuthState`) is a known race against + /// the guest address-picker sheet (see + /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md) - + /// running it a second time back-to-back inside every test (once + /// here, once again inside `reachGuestHome`'s own callers) doubled + /// exposure to that race and broke tests that were previously stable. + override class func setUp() { + super.setUp() + let app = XCUIApplication() + app.launch() + app.logoutIfAuthenticated() + } + override func setUpWithError() throws { continueAfterFailure = false } - /// Authenticated-flow tests no longer log out after themselves (see - /// decisions/2026-09-11-ui-test-shared-login-session.md), so guest - /// tests must force a clean logged-out state themselves right after - /// launch, rather than assume one. - private func launchGuestApp() -> XCUIApplication { + func testHomeRendersSearchAndGreeting() throws { let app = XCUIApplication() app.launch() - app.logoutIfAuthenticated() - return app - } - - func testHomeRendersSearchAndGreeting() throws { - let app = launchGuestApp() XCTAssertTrue(app.reachGuestHome(), "Never reached Home's search field") XCTAssertTrue(app.staticTexts["ENTREGAR EM:"].exists) @@ -31,7 +39,8 @@ final class HomeGuestFlowTests: XCTestCase { } func testHomeShowsDefaultCategoryChip() throws { - let app = launchGuestApp() + let app = XCUIApplication() + app.launch() XCTAssertTrue(app.reachGuestHome()) XCTAssertTrue(app.staticTexts["Categories"].exists) @@ -39,7 +48,8 @@ final class HomeGuestFlowTests: XCTestCase { } func testHomeShowsGuestLocationPromptWhenNoLocationChosen() throws { - let app = launchGuestApp() + let app = XCUIApplication() + app.launch() XCTAssertTrue(app.reachGuestHome()) // No guest state/city selected -> Home's own inline message, not @@ -48,7 +58,8 @@ final class HomeGuestFlowTests: XCTestCase { } func testTappingAddressChipReopensLocationPicker() throws { - let app = launchGuestApp() + let app = XCUIApplication() + app.launch() XCTAssertTrue(app.reachGuestHome()) // The "ENTREGAR EM:" address button re-opens the same address diff --git a/PediFoodsUITests/OrderDetailsFlowTests.swift b/PediFoodsUITests/OrderDetailsFlowTests.swift index 5a3a987..b1c3b82 100644 --- a/PediFoodsUITests/OrderDetailsFlowTests.swift +++ b/PediFoodsUITests/OrderDetailsFlowTests.swift @@ -21,8 +21,14 @@ final class OrderDetailsFlowTests: XCTestCase { XCTAssertTrue(app.staticTexts["Meus Pedidos"].waitForExistence(timeout: 10)) // "Ver Detalhes" appears once per order card; this account has real - // order history (confirmed in AuthenticatedProfileNavigationTests). - let detailsButton = app.buttons["Ver Detalhes"] + // order history (confirmed in AuthenticatedProfileNavigationTests) + // which has grown to more than one order across many manual and + // automated runs, so an exact-match subscript now resolves to + // multiple elements and throws on .tap() ("Find single matching + // element"). .firstMatch tolerates any count - the test only + // needs *an* order's details screen to be reachable, not a + // specific one. + let detailsButton = app.buttons.matching(NSPredicate(format: "label == %@", "Ver Detalhes")).firstMatch XCTAssertTrue(detailsButton.waitForExistence(timeout: 10), "No orders with a 'Ver Detalhes' action found") detailsButton.tap() diff --git a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift index 068bfa6..d49eb03 100644 --- a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift +++ b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift @@ -3,23 +3,32 @@ import XCTest /// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) and /// the auth intro screen (`LoginView`) it opens. final class ProfileLoggedOutFlowTests: XCTestCase { + /// Authenticated-flow tests no longer log out after themselves (see + /// decisions/2026-09-11-ui-test-shared-login-session.md), so this + /// class must force a clean logged-out state before its tests run, + /// rather than assume one. Done once per class, not once per test: + /// the underlying navigate-to-Profile-tab dance + /// (`reachProfileTabRegardlessOfAuthState`) is a known race against + /// the guest address-picker sheet (see + /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md) - + /// running it a second time back-to-back inside every test (once + /// here, once again inside `reachLoggedOutProfile`'s own callers) + /// doubled exposure to that race and broke tests that were + /// previously stable. + override class func setUp() { + super.setUp() + let app = XCUIApplication() + app.launch() + app.logoutIfAuthenticated() + } + override func setUpWithError() throws { continueAfterFailure = false } - /// Authenticated-flow tests no longer log out after themselves (see - /// decisions/2026-09-11-ui-test-shared-login-session.md), so - /// logged-out tests must force a clean state themselves right after - /// launch, rather than assume one. - private func launchLoggedOutApp() -> XCUIApplication { + func testLoggedOutProfileShowsLoginPrompt() throws { let app = XCUIApplication() app.launch() - app.logoutIfAuthenticated() - return app - } - - func testLoggedOutProfileShowsLoginPrompt() throws { - let app = launchLoggedOutApp() XCTAssertTrue(app.reachLoggedOutProfile(), "Never reached the logged-out profile screen") XCTAssertTrue(app.staticTexts["Entre na sua conta"].exists) @@ -32,7 +41,8 @@ final class ProfileLoggedOutFlowTests: XCTestCase { /// a working way back out (its `LCENavigationView` back button, which /// sets `root = .main`). func testAuthIntroScreenShowsChoicesAndCanGoBack() throws { - let app = launchLoggedOutApp() + let app = XCUIApplication() + app.launch() XCTAssertTrue(app.reachLoggedOutProfile()) app.buttons["Entrar ou Cadastrar"].tap() @@ -54,7 +64,8 @@ final class ProfileLoggedOutFlowTests: XCTestCase { /// app's standard `AppBackButtonIcon` via `LCENavigationView`, not the /// oversized iOS 26 system glass back button. func testPushedAuthScreensUseAppBackButton() throws { - let app = launchLoggedOutApp() + let app = XCUIApplication() + app.launch() XCTAssertTrue(app.reachLoggedOutProfile()) app.buttons["Entrar ou Cadastrar"].tap() From d06e5013a8218ef420fc58dc0c5c8e2d0bba031a Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:04:25 -0300 Subject: [PATCH 07/17] test(uitests): drain accumulated cart once per class before checkout tests The cart is on-device state (CartState/SessionStateStore), not purely server-side - it survives app relaunches on the same simulator and has been accumulating real quantities across every manual and automated run against the 'MARIBA' test store, with no reset in between. No one-tap 'empty cart' UI action exists; the app's only full-clear path (the 'Trocar de loja?' alert's 'Limpar carrinho e adicionar') only fires when switching to a genuinely different store, which this suite never does. Added CartCheckoutFlowTests.setUp() (class) to drain the cart via its own per-item 'minus' control in a capped loop before this class's tests run, once per class rather than once per test, so every run starts from a known-empty cart. See decisions/2026-09-11-ui-test-shared-login-session.md follow-up. --- PediFoodsUITests/CartCheckoutFlowTests.swift | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index 77efe88..d13cd86 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -6,6 +6,45 @@ import XCTest /// so Home shows a real store list - see /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. final class CartCheckoutFlowTests: XCTestCase { + /// The cart is on-device state (`CartState`/`SessionStateStore`, see + /// `AppState.swift`), not server-side - it persists across app + /// relaunches on this simulator just like the login session (see + /// decisions/2026-09-11-ui-test-shared-login-session.md), and keeps + /// accumulating real quantities across every manual and automated run + /// against the "MARIBA" test store. There's no direct "empty cart" UI + /// action - the "Trocar de loja?" alert's "Limpar carrinho e + /// adicionar" (`StoreDetailView.swift`) only fires for a genuinely + /// different store (`shouldAskForStoreSwitch`, + /// `StoreDetailView+Logic.swift`), which this suite never triggers + /// since every test targets the same store. So: drain the cart via + /// its own per-item "minus" control, once per class (not once per + /// test - each drain can take several taps), so every run starts from + /// a known-empty cart instead of an ever-growing one. + override class func setUp() { + super.setUp() + let app = XCUIApplication() + app.launch() + app.ensureLoggedIn() + + let cartTab = app.images.matching(identifier: "cart.fill").firstMatch + guard cartTab.waitForExistence(timeout: 10) else { return } + cartTab.tap() + guard app.staticTexts["Meu Carrinho"].waitForExistence(timeout: 10) else { return } + + // No "set quantity" input exists in CartView - only per-tap + // increment/decrement - so draining is inherently one tap per + // unit. Capped rather than unbounded so a genuinely stuck cart + // fails fast instead of hanging the whole class. + let minusButton = app.images.matching(identifier: "minus").firstMatch + var remainingTaps = 500 + while app.staticTexts["Seu carrinho está vazio"].exists == false, remainingTaps > 0 { + guard minusButton.waitForExistence(timeout: 3) else { break } + minusButton.tap() + usleep(150_000) + remainingTaps -= 1 + } + } + override func setUpWithError() throws { continueAfterFailure = false } From 9031f617a9f86666ef0009ec42b7f725f12d06ac Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:19:52 -0300 Subject: [PATCH 08/17] fix: match NotificationServiceExtension's CURRENT_PROJECT_VERSION to the app's The extension's Debug/Release configs had CURRENT_PROJECT_VERSION = 1 (untouched Xcode default) while the main app target uses 2026.0.25.0.0.1, triggering 'The CFBundleVersion of an app extension (1) must match that of its containing parent app' on every build/test run. Both targets now build with the same CURRENT_PROJECT_VERSION. --- PediFoods.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PediFoods.xcodeproj/project.pbxproj b/PediFoods.xcodeproj/project.pbxproj index c2686b8..3b3d3f0 100644 --- a/PediFoods.xcodeproj/project.pbxproj +++ b/PediFoods.xcodeproj/project.pbxproj @@ -1111,7 +1111,7 @@ CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -1242,7 +1242,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; From 238b2ac7ba85e8d584c89b5c15458a207f05f6aa Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:21:01 -0300 Subject: [PATCH 09/17] chore: bump CURRENT_PROJECT_VERSION to 2026.09.11.0.0.1 2026.0.25.0.0.1 is already live on the App Store. Bumped both the app and NotificationServiceExtension targets together, keeping them matched. --- PediFoods.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PediFoods.xcodeproj/project.pbxproj b/PediFoods.xcodeproj/project.pbxproj index 3b3d3f0..8b6705d 100644 --- a/PediFoods.xcodeproj/project.pbxproj +++ b/PediFoods.xcodeproj/project.pbxproj @@ -1001,7 +1001,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; + CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; ENABLE_PREVIEWS = YES; @@ -1046,7 +1046,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; + CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; ENABLE_PREVIEWS = YES; @@ -1111,7 +1111,7 @@ CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; + CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -1242,7 +1242,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; + CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; From 1a7d5c3d51a056d0234e3e561fce1664b3596394 Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:21:43 -0300 Subject: [PATCH 10/17] test(uitests): replace broken scratch-path screenshots with debugDescription dumps All three failure-path screenshot saves in this file wrote to a hardcoded path from a different machine/session (/private/tmp/claude-501/-Users-loverde-co-.../scratchpad), which doesn't exist under the Mac mini runner's user account - try? silently swallowed the write failure, so no diagnostic evidence was actually produced on CI failures. Replaced with app.debugDescription printed to stdout, which lands directly in the CI log with no extra file access needed - same technique already used in decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. Next CartCheckoutFlowTests failure will show the actual accessibility hierarchy at the point of timeout. --- PediFoodsUITests/CartCheckoutFlowTests.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index d13cd86..a8a9497 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -106,8 +106,8 @@ final class CartCheckoutFlowTests: XCTestCase { app.swipeDown() } if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false { - let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" - try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/after_alterar_dismiss.png")) + print("=== Never returned to Checkout after Alterar dismiss - accessibility hierarchy ===") + print(app.debugDescription) } XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout") @@ -180,8 +180,8 @@ final class CartCheckoutFlowTests: XCTestCase { // a plain static text. let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch if storeCard.waitForExistence(timeout: 15) == false { - let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" - try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/no_store_card.png")) + print("=== No MARIBA store card on Home - accessibility hierarchy ===") + print(app.debugDescription) } XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home") storeCard.tap() @@ -201,8 +201,15 @@ final class CartCheckoutFlowTests: XCTestCase { // directly instead - same fix pattern as the cart-tab icon. let addButton = app.images.matching(identifier: "plus").firstMatch if addButton.waitForExistence(timeout: 25) == false { - let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" - try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/store_detail_timeout40.png")) + // A hardcoded scratch-path screenshot write here was silently + // no-oping on the CI runner (try? swallowed a write failure + // into a directory that doesn't exist on this machine's user + // account) - printing the accessibility hierarchy instead + // shows up directly in the CI log with no extra file access + // needed, same technique already used in + // decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. + print("=== Store Detail timeout - accessibility hierarchy ===") + print(app.debugDescription) } XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded") addButton.tap() From 3f99ebcba1d64434802a1e6334f8f99b21dbdf5f Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:44:02 -0300 Subject: [PATCH 11/17] test(uitests): write failure diagnostics to a file, not print() Confirmed (pasted full CI log around a timeout, searched for the print()'d marker string, found nothing) that plain print() from inside a UI test never reaches fastlane's xcodebuild log output on this runner - its formatter only relays lines matching its own known patterns and drops everything else. CartCheckoutFlowTests now writes app.debugDescription to NSTemporaryDirectory()+'pedifoods_uitest_diag.log' on each of its three failure points (shared dumpDiagnostics helper, appending), clearing any stale copy in class setUp(). test.yml gained a step after 'Run tests with coverage' (if: always()) that cats that file when present - TMPDIR is stable for the whole CI job, unlike the per-run-hashed workspace path that broke the original screenshot attempt. See decisions/2026-09-11-ui-test-shared-login-session.md follow-up. --- .gitea/workflows/test.yml | 9 ++++ PediFoodsUITests/CartCheckoutFlowTests.swift | 47 ++++++++++++++------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index f0e04d0..2dea663 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -24,3 +24,12 @@ jobs: - name: Run tests with coverage run: fastlane tests + + - name: Show UI test failure diagnostics (if any) + if: always() + run: | + if [ -f "$TMPDIR/pedifoods_uitest_diag.log" ]; then + cat "$TMPDIR/pedifoods_uitest_diag.log" + else + echo "No UI test diagnostic log was written." + fi diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index a8a9497..5e22364 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -6,6 +6,33 @@ import XCTest /// so Home shows a real store list - see /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. final class CartCheckoutFlowTests: XCTestCase { + /// Plain `print()` from inside a UI test does not reach the CI log on + /// this runner - fastlane's xcodebuild log formatter only relays + /// lines matching its own known patterns (Test Suite/Test Case/etc.) + /// and silently drops anything else, confirmed by pasting the full + /// log around a timeout and finding no trace of a print()'d marker + /// string. Writing to a file `cat`'d by a later CI step + /// (`.gitea/workflows/test.yml`) works regardless. `NSTemporaryDirectory()` + /// resolves to the CI job's `$TMPDIR`, consistent for the whole job + /// (both the xcodebuild-spawned test process and the later shell step + /// run under the same user session) - unlike the fixed scratch path + /// this replaced, which was hardcoded to a different machine/session + /// and silently no-op'd here. + static let diagnosticLogPath = NSTemporaryDirectory() + "pedifoods_uitest_diag.log" + + private static func dumpDiagnostics(_ title: String, app: XCUIApplication) { + let entry = "\n=== \(title) ===\n\(app.debugDescription)\n" + guard let data = entry.data(using: .utf8) else { return } + if FileManager.default.fileExists(atPath: diagnosticLogPath), + let handle = FileHandle(forWritingAtPath: diagnosticLogPath) { + handle.seekToEndOfFile() + handle.write(data) + handle.closeFile() + } else { + FileManager.default.createFile(atPath: diagnosticLogPath, contents: data) + } + } + /// The cart is on-device state (`CartState`/`SessionStateStore`, see /// `AppState.swift`), not server-side - it persists across app /// relaunches on this simulator just like the login session (see @@ -22,6 +49,10 @@ final class CartCheckoutFlowTests: XCTestCase { /// a known-empty cart instead of an ever-growing one. override class func setUp() { super.setUp() + // Start each run with a clean diagnostic log, not a leftover from + // a previous one. + try? FileManager.default.removeItem(atPath: diagnosticLogPath) + let app = XCUIApplication() app.launch() app.ensureLoggedIn() @@ -106,8 +137,7 @@ final class CartCheckoutFlowTests: XCTestCase { app.swipeDown() } if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false { - print("=== Never returned to Checkout after Alterar dismiss - accessibility hierarchy ===") - print(app.debugDescription) + Self.dumpDiagnostics("Never returned to Checkout after Alterar dismiss", app: app) } XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout") @@ -180,8 +210,7 @@ final class CartCheckoutFlowTests: XCTestCase { // a plain static text. let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch if storeCard.waitForExistence(timeout: 15) == false { - print("=== No MARIBA store card on Home - accessibility hierarchy ===") - print(app.debugDescription) + Self.dumpDiagnostics("No MARIBA store card on Home", app: app) } XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home") storeCard.tap() @@ -201,15 +230,7 @@ final class CartCheckoutFlowTests: XCTestCase { // directly instead - same fix pattern as the cart-tab icon. let addButton = app.images.matching(identifier: "plus").firstMatch if addButton.waitForExistence(timeout: 25) == false { - // A hardcoded scratch-path screenshot write here was silently - // no-oping on the CI runner (try? swallowed a write failure - // into a directory that doesn't exist on this machine's user - // account) - printing the accessibility hierarchy instead - // shows up directly in the CI log with no extra file access - // needed, same technique already used in - // decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. - print("=== Store Detail timeout - accessibility hierarchy ===") - print(app.debugDescription) + Self.dumpDiagnostics("Store Detail timeout - product list never loaded", app: app) } XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded") addButton.tap() From 6da8b05cfb43064847404f0d2caf2a5348f848ca Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 10:57:48 -0300 Subject: [PATCH 12/17] test(uitests): embed live-state diagnostics in assertion messages, not a file The file-based diagnostic dump never worked: PediFoodsUITests-Runner runs as an app inside the simulator, sandboxed to that device's own container filesystem. NSTemporaryDirectory() called from test code resolves inside the simulator's data container, invisible to the host shell's $TMPDIR a later CI step tried to cat - confirmed by the new step printing 'No UI test diagnostic log was written.' every time. Replaced with quickDiagnostics(_:), which builds a short live-state summary (closed-store snackbar, spinning activity indicator, any open alert, visible 'erro' text, first ~15 visible static texts) and interpolates it directly into each of the three XCTAssertTrue failure messages - the one channel already confirmed working end to end on every run. message: is @autoclosure, so this costs nothing when the assertion passes. Removed the dead file-write helper and the now-unused CI step. See decisions/2026-09-11-ui-test-shared-login-session.md follow-up. --- .gitea/workflows/test.yml | 9 -- PediFoodsUITests/CartCheckoutFlowTests.swift | 90 +++++++++++--------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 2dea663..f0e04d0 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -24,12 +24,3 @@ jobs: - name: Run tests with coverage run: fastlane tests - - - name: Show UI test failure diagnostics (if any) - if: always() - run: | - if [ -f "$TMPDIR/pedifoods_uitest_diag.log" ]; then - cat "$TMPDIR/pedifoods_uitest_diag.log" - else - echo "No UI test diagnostic log was written." - fi diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index 5e22364..b25a156 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -6,31 +6,45 @@ import XCTest /// so Home shows a real store list - see /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. final class CartCheckoutFlowTests: XCTestCase { - /// Plain `print()` from inside a UI test does not reach the CI log on - /// this runner - fastlane's xcodebuild log formatter only relays - /// lines matching its own known patterns (Test Suite/Test Case/etc.) - /// and silently drops anything else, confirmed by pasting the full - /// log around a timeout and finding no trace of a print()'d marker - /// string. Writing to a file `cat`'d by a later CI step - /// (`.gitea/workflows/test.yml`) works regardless. `NSTemporaryDirectory()` - /// resolves to the CI job's `$TMPDIR`, consistent for the whole job - /// (both the xcodebuild-spawned test process and the later shell step - /// run under the same user session) - unlike the fixed scratch path - /// this replaced, which was hardcoded to a different machine/session - /// and silently no-op'd here. - static let diagnosticLogPath = NSTemporaryDirectory() + "pedifoods_uitest_diag.log" - - private static func dumpDiagnostics(_ title: String, app: XCUIApplication) { - let entry = "\n=== \(title) ===\n\(app.debugDescription)\n" - guard let data = entry.data(using: .utf8) else { return } - if FileManager.default.fileExists(atPath: diagnosticLogPath), - let handle = FileHandle(forWritingAtPath: diagnosticLogPath) { - handle.seekToEndOfFile() - handle.write(data) - handle.closeFile() - } else { - FileManager.default.createFile(atPath: diagnosticLogPath, contents: data) + /// Two earlier diagnostic attempts for the timeouts below both failed + /// silently: plain `print()` never reached fastlane's xcodebuild log + /// (its formatter only relays lines matching its own known patterns), + /// and a file written via `NSTemporaryDirectory()` was never found by + /// a later CI step's `cat` either - because a UI test target's code + /// runs as `PediFoodsUITests-Runner`, an app *inside the simulator*, + /// sandboxed to that simulator device's own container filesystem; + /// `NSTemporaryDirectory()` there resolves to a path under + /// `.../CoreSimulator/Devices//data/...`, invisible to the + /// host shell's `$TMPDIR` a later workflow step reads from. + /// + /// A `XCTAssertTrue` failure *message* is the one diagnostic channel + /// already confirmed working end to end every single run (the + /// "Store Detail's product list never loaded" text itself always + /// showed up). Its `message` parameter is `@autoclosure`, evaluated + /// only on actual failure - so building a short live-state summary + /// and interpolating it into the message costs nothing when the + /// assertion passes, and needs no file access or CI plumbing at all. + private func quickDiagnostics(_ app: XCUIApplication) -> String { + var signals: [String] = [] + if app.staticTexts["Loja fechada no momento."].exists { + signals.append("closed-store snackbar visible") } + if app.activityIndicators.firstMatch.exists { + signals.append("activity indicator still spinning") + } + if app.alerts.firstMatch.exists { + signals.append("alert showing: \(app.alerts.firstMatch.label)") + } + let errorText = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", "erro")).firstMatch + if errorText.exists { + signals.append("error text visible: \(errorText.label)") + } + let visibleTexts = app.staticTexts.allElementsBoundByIndex + .prefix(15) + .map(\.label) + .joined(separator: " | ") + signals.append("visible static texts: [\(visibleTexts)]") + return signals.joined(separator: "; ") } /// The cart is on-device state (`CartState`/`SessionStateStore`, see @@ -49,10 +63,6 @@ final class CartCheckoutFlowTests: XCTestCase { /// a known-empty cart instead of an ever-growing one. override class func setUp() { super.setUp() - // Start each run with a clean diagnostic log, not a leftover from - // a previous one. - try? FileManager.default.removeItem(atPath: diagnosticLogPath) - let app = XCUIApplication() app.launch() app.ensureLoggedIn() @@ -136,10 +146,10 @@ final class CartCheckoutFlowTests: XCTestCase { if dismissed == false { app.swipeDown() } - if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false { - Self.dumpDiagnostics("Never returned to Checkout after Alterar dismiss", app: app) - } - XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout") + XCTAssertTrue( + app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), + "Never returned to Checkout. \(quickDiagnostics(app))" + ) // Deliberately not tapping "Confirmar e Pagar" - see doc comment. } @@ -209,10 +219,10 @@ final class CartCheckoutFlowTests: XCTestCase { // distance + status all combine into its accessibility label), not // a plain static text. let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch - if storeCard.waitForExistence(timeout: 15) == false { - Self.dumpDiagnostics("No MARIBA store card on Home", app: app) - } - XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home") + XCTAssertTrue( + storeCard.waitForExistence(timeout: 15), + "Expected store card never appeared on Home. \(quickDiagnostics(app))" + ) storeCard.tap() // A plain (non-addon, non-pizza) product row's "+" control adds @@ -229,10 +239,10 @@ final class CartCheckoutFlowTests: XCTestCase { // keeps identifier "plus" regardless of quantity, so target that // directly instead - same fix pattern as the cart-tab icon. let addButton = app.images.matching(identifier: "plus").firstMatch - if addButton.waitForExistence(timeout: 25) == false { - Self.dumpDiagnostics("Store Detail timeout - product list never loaded", app: app) - } - XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded") + XCTAssertTrue( + addButton.waitForExistence(timeout: 25), + "Store Detail's product list never loaded. \(quickDiagnostics(app))" + ) addButton.tap() // Once the cart has items, the tab bar button's accessible label From ea6a3da78b943d35b4b937e7f03e3e530bec1e65 Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 11:02:02 -0300 Subject: [PATCH 13/17] fix(uitests): replace illegal key path with a closure in quickDiagnostics '.map(\.label)' failed to build: XCUIElement.label is @MainActor-isolated, and a bare key path literal must be formable from any isolation context, which a main-actor-isolated property can't satisfy. This was a hard compiler error (not just a concurrency warning), and it cascaded into a flood of secondary diagnostics across the whole file/batch compile unit, obscuring the actual cause. Replaced with a closure ('{ $0.label }'), which isn't subject to that restriction. --- PediFoodsUITests/CartCheckoutFlowTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index b25a156..9028e6d 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -39,9 +39,12 @@ final class CartCheckoutFlowTests: XCTestCase { if errorText.exists { signals.append("error text visible: \(errorText.label)") } + // A bare `\.label` key path literal is illegal here - `label` is + // @MainActor-isolated, and a key path must be formable from any + // isolation context. A closure isn't subject to that restriction. let visibleTexts = app.staticTexts.allElementsBoundByIndex .prefix(15) - .map(\.label) + .map { $0.label } .joined(separator: " | ") signals.append("visible static texts: [\(visibleTexts)]") return signals.joined(separator: "; ") From 2ebe97e078fb5989115f561ce0da82914825f2c2 Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 11:14:08 -0300 Subject: [PATCH 14/17] fix(uitests): give the product add button an explicit accessibility identifier quickDiagnostics finally showed the real state at the timeout: Store Detail had genuinely loaded (store header, category tabs, a real priced product 'Alcatra' R$ 14,00 all visible) - 'product list never loaded' was simply the wrong diagnosis. The test was matching app.images.matching(identifier: "plus") against the product row's add button, relying entirely on SF Symbol systemName being exposed as an implicit accessibility identifier - which wasn't holding on this Xcode 26/iOS 26 CI environment, and is inherently ambiguous anyway since CartView's own quantity stepper reuses the same "plus" systemName. Added a real .accessibilityIdentifier("storeDetailProductAddButton") to the product row's add Button in StoreDetailView+Components.swift (an app-source change, not just a test workaround), and updated the test to match on that instead. See decisions/2026-09-11-ui-test-shared-login-session.md follow-up. --- .../Main/StoreDetailView+Components.swift | 5 ++++ PediFoodsUITests/CartCheckoutFlowTests.swift | 28 +++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/PediFoods/Views/Main/StoreDetailView+Components.swift b/PediFoods/Views/Main/StoreDetailView+Components.swift index 379aa93..5044940 100644 --- a/PediFoods/Views/Main/StoreDetailView+Components.swift +++ b/PediFoods/Views/Main/StoreDetailView+Components.swift @@ -345,6 +345,11 @@ extension StoreDetailView { .disabled(isStoreOpen == false) .opacity(isStoreOpen ? 1 : 0.65) .offset(x: 7, y: 7) + // Explicit, unique identifier for UI tests - the inner + // "plus" SF Symbol's implicit/auto-generated identifier is + // ambiguous (the same systemName is reused for CartView's + // quantity stepper) and unreliable to match against. + .accessibilityIdentifier("storeDetailProductAddButton") } } .padding(12) diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index 9028e6d..47b5aaa 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -228,20 +228,20 @@ final class CartCheckoutFlowTests: XCTestCase { ) storeCard.tap() - // A plain (non-addon, non-pizza) product row's "+" control adds - // directly with no sheet. Once a product's quantity is > 0, its - // outer Button's identifier moves off itself - the same badge- - // merging bug already fixed for the tab bar's cart icon - because - // the row's own quantity Text becomes the Button's accessible - // label/identity instead. The QA account's cart has genuinely - // accumulated real quantities across many runs today, so - // app.buttons.matching(identifier: "plus") stopped matching once - // the first several products all had quantity > 0 (confirmed via - // screenshot: the "+" controls were clearly visible on screen - // while the buttons-only query found nothing). The nested Image - // keeps identifier "plus" regardless of quantity, so target that - // directly instead - same fix pattern as the cart-tab icon. - let addButton = app.images.matching(identifier: "plus").firstMatch + // The product row's add button carries an explicit + // `.accessibilityIdentifier("storeDetailProductAddButton")` + // (StoreDetailView+Components.swift) - added after discovering + // that matching on the inner "plus" SF Symbol's implicit/ + // auto-generated identifier was unreliable: `quickDiagnostics`'s + // visible-text dump showed a real product ("Alcatra", R$ 14,00) + // clearly rendered on screen while `images.matching(identifier: + // "plus")` still found nothing, and the same "plus" systemName is + // separately reused by CartView's quantity stepper, making it an + // ambiguous identifier to search by in the first place. Matching + // the button itself (not a nested image) also sidesteps the + // quantity-badge accessibility-label-merging issue that affected + // the tab bar's cart icon elsewhere in this suite. + let addButton = app.buttons.matching(identifier: "storeDetailProductAddButton").firstMatch XCTAssertTrue( addButton.waitForExistence(timeout: 25), "Store Detail's product list never loaded. \(quickDiagnostics(app))" From 6fead38ad7bd2a2f8eb296c75da23edce5ae8c2e Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 11:21:30 -0300 Subject: [PATCH 15/17] test(uitests): cover both add-on and plain-add product flows The original helper only ever tapped the *first* product's add button and assumed it went straight to Cart. That's the same single-path assumption that produced the wrong 'product list never loaded' diagnosis - the first product on this catalog ('Alcatra') has add-ons and opens ProductDetailSheet ('Detalhes') instead of adding directly. Fixing just the button selector would have hidden this and failed differently on the next run. Renamed reachCheckoutWithOneItem to reachCheckoutWithProducts(_:count: Int = 3) and added addProduct(atIndex:in:), which taps the Nth product's add button and branches on the real outcome: if ProductDetailSheet appears, confirms via its own main action button ('Atualizar...') and waits for it to dismiss; otherwise the product already added directly. Loops over the first 3 products (capped by the actual button count) so both flows are exercised on every run, regardless of which specific products the real catalog happens to give add-ons to. See decisions/2026-09-11-ui-test-shared-login-session.md follow-up. --- PediFoodsUITests/CartCheckoutFlowTests.swift | 67 ++++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/PediFoodsUITests/CartCheckoutFlowTests.swift b/PediFoodsUITests/CartCheckoutFlowTests.swift index 47b5aaa..f5ca87e 100644 --- a/PediFoodsUITests/CartCheckoutFlowTests.swift +++ b/PediFoodsUITests/CartCheckoutFlowTests.swift @@ -98,7 +98,7 @@ final class CartCheckoutFlowTests: XCTestCase { func testAddProductToCartAndReachCheckout() throws { let app = XCUIApplication() app.launch() - try reachCheckoutWithOneItem(app) + try reachCheckoutWithProducts(app) } /// Exercises CheckoutView's payment-method selection and the address @@ -108,7 +108,7 @@ final class CartCheckoutFlowTests: XCTestCase { func testCheckoutPaymentMethodSelectionAndAddressAlterar() throws { let app = XCUIApplication() app.launch() - try reachCheckoutWithOneItem(app) + try reachCheckoutWithProducts(app) XCTAssertTrue(app.staticTexts["MÉTODO DE PAGAMENTO"].waitForExistence(timeout: 5)) @@ -167,7 +167,7 @@ final class CartCheckoutFlowTests: XCTestCase { func testConfirmarEPagarWithCreditCardOpensCardSelectionWithoutSubmitting() throws { let app = XCUIApplication() app.launch() - try reachCheckoutWithOneItem(app) + try reachCheckoutWithProducts(app) let creditCardRow = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "Cartão de Crédito")).firstMatch XCTAssertTrue(creditCardRow.waitForExistence(timeout: 5), "This store is expected to offer Cartão de Crédito") @@ -209,7 +209,49 @@ final class CartCheckoutFlowTests: XCTestCase { app.swipeDown() } - private func reachCheckoutWithOneItem(_ app: XCUIApplication) throws { + /// Taps the Nth product row's add button (0-based, top to bottom) and + /// handles both real outcomes on this catalog: a product with add-ons + /// (e.g. "Alcatra", the first product) opens `ProductDetailSheet` + /// ("Detalhes") and needs its own confirm tap; a plain product (no + /// `addonGroups`) adds directly with no sheet at all + /// (`StoreDetailView+Components.swift`). Which products have add-ons + /// is real catalog data, not something this suite controls, so both + /// paths must be handled rather than assumed - asserting "no sheet + /// appeared" for a product that genuinely has add-ons is exactly the + /// wrong-diagnosis mistake this suite already made once for this same + /// screen (see decisions/2026-09-11-ui-test-shared-login-session.md). + /// `ProductDetailSheet` requires no addon selection to confirm - + /// `quantity` defaults to 1 and addons default to none, so its main + /// action button ("Atualizar • ") is always tappable as-is. + private func addProduct(atIndex index: Int, in app: XCUIApplication) { + let addButtons = app.buttons.matching(identifier: "storeDetailProductAddButton") + let button = addButtons.element(boundBy: index) + XCTAssertTrue( + button.waitForExistence(timeout: 25), + "Product add button at index \(index) never appeared. \(quickDiagnostics(app))" + ) + button.tap() + + if app.staticTexts["Detalhes"].waitForExistence(timeout: 3) { + let confirmButton = app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Atualizar")).firstMatch + XCTAssertTrue( + confirmButton.waitForExistence(timeout: 5), + "ProductDetailSheet's confirm button never appeared for product at index \(index). \(quickDiagnostics(app))" + ) + confirmButton.tap() + XCTAssertFalse( + app.staticTexts["Detalhes"].waitForExistence(timeout: 3), + "ProductDetailSheet never dismissed after confirming product at index \(index)" + ) + } + } + + /// Adds the first `count` products from Store Detail's list, top to + /// bottom, then reaches Checkout. Defaults to 3 so both the add-on + /// flow (first product) and the plain-add flow (later products) are + /// both exercised on every run, not just whichever one the first + /// product happens to be. + private func reachCheckoutWithProducts(_ app: XCUIApplication, count: Int = 3) throws { XCTAssertTrue(app.ensureLoggedIn(), "Login never completed") // ensureLoggedIn can land on whichever tab it detected the @@ -237,16 +279,17 @@ final class CartCheckoutFlowTests: XCTestCase { // clearly rendered on screen while `images.matching(identifier: // "plus")` still found nothing, and the same "plus" systemName is // separately reused by CartView's quantity stepper, making it an - // ambiguous identifier to search by in the first place. Matching - // the button itself (not a nested image) also sidesteps the - // quantity-badge accessibility-label-merging issue that affected - // the tab bar's cart icon elsewhere in this suite. - let addButton = app.buttons.matching(identifier: "storeDetailProductAddButton").firstMatch + // ambiguous identifier to search by in the first place. + let addButtons = app.buttons.matching(identifier: "storeDetailProductAddButton") XCTAssertTrue( - addButton.waitForExistence(timeout: 25), + addButtons.firstMatch.waitForExistence(timeout: 25), "Store Detail's product list never loaded. \(quickDiagnostics(app))" ) - addButton.tap() + + let actualCount = min(count, addButtons.count) + for index in 0.. Date: Fri, 11 Sep 2026 13:24:34 -0300 Subject: [PATCH 16/17] fix: force sigh to renew provisioning profiles against current certs Archive failed with 'Provisioning profile ... doesn't include signing certificate iPhone Distribution: Loverde Company LTDA (K4E5BZMM4V)' for both the app and NotificationServiceExtension targets, once ASC auth itself was working. Without force: true, get_provisioning_profile just downloads whatever profile already exists on Apple's servers as-is - this CI keychain's distribution certificate differs from whatever the existing profiles were last generated against. force: true makes sigh regenerate them against the team's currently valid certificates. --- fastlane/Fastfile | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7d6a873..bc5d738 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -37,7 +37,17 @@ end lane :beta do |options| desc "Build and upload to TestFlight" - get_provisioning_profile(api_key_path: "fastlane/apikey.json") + # force: true - without it, sigh just downloads whatever profile + # already exists on Apple's servers as-is. This CI keychain's + # distribution certificate is a different one than whatever the + # existing profile was last generated against, so a plain (non-forced) + # fetch produced a profile that doesn't include this machine's cert - + # "Provisioning profile ... doesn't include signing certificate + # 'iPhone Distribution: Loverde Company LTDA (K4E5BZMM4V)'" at archive + # time. Forcing a renewal makes sigh regenerate the profile against + # the team's currently valid certificates instead of reusing a stale + # one. + get_provisioning_profile(api_key_path: "fastlane/apikey.json", force: true) assemble @@ -55,7 +65,17 @@ lane :release do |options| # if you have an apikey.json file (https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api), fastlane can automatically fetch certificates and the ASC authentication information #get_certificates(api_key_path: "fastlane/apikey.json") - get_provisioning_profile(api_key_path: "fastlane/apikey.json") + # force: true - without it, sigh just downloads whatever profile + # already exists on Apple's servers as-is. This CI keychain's + # distribution certificate is a different one than whatever the + # existing profile was last generated against, so a plain (non-forced) + # fetch produced a profile that doesn't include this machine's cert - + # "Provisioning profile ... doesn't include signing certificate + # 'iPhone Distribution: Loverde Company LTDA (K4E5BZMM4V)'" at archive + # time. Forcing a renewal makes sigh regenerate the profile against + # the team's currently valid certificates instead of reusing a stale + # one. + get_provisioning_profile(api_key_path: "fastlane/apikey.json", force: true) assemble From f973926aa400967e5ea45d08cf6fa2620d8cfdd4 Mon Sep 17 00:00:00 2001 From: "Developer @ Loverde Company" Date: Fri, 11 Sep 2026 13:36:56 -0300 Subject: [PATCH 17/17] fix: renew provisioning profiles by exact name, for both app and extension targets The prior force:true fix only touched the main app's profile and let sigh name it ' AppStore' - a different name than the one manual signing expects (PROVISIONING_PROFILE_SPECIFIER = 'LC Prov PediFoods Dist Profile' / 'LC Prov PediFoods Dist Push Profile', baked into PediFoods.xcodeproj per target). Xcode resolves profiles by exact Name match against installed .mobileprovision files, so it never found sigh's freshly-created one and kept falling back to the stale, wrong-certificate profile under the old name - the extension's profile was never touched at all, since sigh only acts on the app_identifier it's given. Added renew_provisioning_profiles (shared by beta and release), calling get_provisioning_profile once per target with an explicit provisioning_name matching what the project actually expects, plus force: true so each is regenerated against the team's current certificates. --- fastlane/Fastfile | 52 +++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bc5d738..183bbfe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -34,20 +34,38 @@ lane :assemble do |options| ) end +# Manual signing (PROVISIONING_PROFILE_SPECIFIER baked directly into +# PediFoods.xcodeproj's build settings, per target) means Xcode resolves +# a profile by matching that exact Name string against installed +# .mobileprovision files - not by bundle identifier alone. Without an +# explicit provisioning_name:, sigh names a freshly (re)created profile +# " AppStore" by default, which doesn't match the +# project's expected names ("LC Prov PediFoods Dist Profile" / "LC Prov +# PediFoods Dist Push Profile") - Xcode then can't find it and falls +# back to whatever stale profile happens to already be installed under +# the old name, which is exactly the doesn't-include-this-cert error +# force: true alone didn't fix. Also needs one call per target: sigh +# only touches the app_identifier it's given, and the +# NotificationServiceExtension has its own separate bundle id. +private_lane :renew_provisioning_profiles do + get_provisioning_profile( + api_key_path: "fastlane/apikey.json", + app_identifier: "com.br.pedifoods.app", + provisioning_name: "LC Prov PediFoods Dist Profile", + force: true + ) + get_provisioning_profile( + api_key_path: "fastlane/apikey.json", + app_identifier: "com.br.pedifoods.app.NotificationService", + provisioning_name: "LC Prov PediFoods Dist Push Profile", + force: true + ) +end + lane :beta do |options| desc "Build and upload to TestFlight" - # force: true - without it, sigh just downloads whatever profile - # already exists on Apple's servers as-is. This CI keychain's - # distribution certificate is a different one than whatever the - # existing profile was last generated against, so a plain (non-forced) - # fetch produced a profile that doesn't include this machine's cert - - # "Provisioning profile ... doesn't include signing certificate - # 'iPhone Distribution: Loverde Company LTDA (K4E5BZMM4V)'" at archive - # time. Forcing a renewal makes sigh regenerate the profile against - # the team's currently valid certificates instead of reusing a stale - # one. - get_provisioning_profile(api_key_path: "fastlane/apikey.json", force: true) + renew_provisioning_profiles assemble @@ -65,17 +83,7 @@ lane :release do |options| # if you have an apikey.json file (https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api), fastlane can automatically fetch certificates and the ASC authentication information #get_certificates(api_key_path: "fastlane/apikey.json") - # force: true - without it, sigh just downloads whatever profile - # already exists on Apple's servers as-is. This CI keychain's - # distribution certificate is a different one than whatever the - # existing profile was last generated against, so a plain (non-forced) - # fetch produced a profile that doesn't include this machine's cert - - # "Provisioning profile ... doesn't include signing certificate - # 'iPhone Distribution: Loverde Company LTDA (K4E5BZMM4V)'" at archive - # time. Forcing a renewal makes sigh regenerate the profile against - # the team's currently valid certificates instead of reusing a stale - # one. - get_provisioning_profile(api_key_path: "fastlane/apikey.json", force: true) + renew_provisioning_profiles assemble