Merge pull request 'feature/ci/gitea-macos-runner' (#49) from feature/ci/gitea-macos-runner into develop

Reviewed-on: Loverde-Company-LTDA/Pedi-Foods-Skip#49
This commit is contained in:
2026-09-11 14:01:30 -03:00
15 changed files with 282 additions and 77 deletions

View File

@@ -20,6 +20,18 @@ jobs:
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${{ github.sha }}.tar.gz" "${{ 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" 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 - name: Write App Store Connect API key
run: | run: |
cat > fastlane/apikey.json <<EOF cat > fastlane/apikey.json <<EOF

View File

@@ -19,5 +19,8 @@ jobs:
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${{ github.sha }}.tar.gz" "${{ 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" 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 - name: Run tests with coverage
run: fastlane tests run: fastlane tests

View File

@@ -34,7 +34,6 @@
29E376FB24A2F19361D5BB6A /* ApiCustomerPayloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246FDD4C8985BF2F5C900001 /* ApiCustomerPayloadModels.swift */; }; 29E376FB24A2F19361D5BB6A /* ApiCustomerPayloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246FDD4C8985BF2F5C900001 /* ApiCustomerPayloadModels.swift */; };
2A1940BB0EDC3FF50127B39A /* SnackbarOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4745C12F14B695C77DFE178 /* SnackbarOverlay.swift */; }; 2A1940BB0EDC3FF50127B39A /* SnackbarOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4745C12F14B695C77DFE178 /* SnackbarOverlay.swift */; };
2B202626C34FC9F5C87C0388 /* ApiOrderModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4625B70E3E8BEEB9D0B2042 /* ApiOrderModels.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 */; }; 2FCB3403A764AD74AC3BD47C /* LegalDocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */; };
33AB0B87867400F3D0C65FA0 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DBC4B9B43C015C4DD411359 /* ApiClient.swift */; }; 33AB0B87867400F3D0C65FA0 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DBC4B9B43C015C4DD411359 /* ApiClient.swift */; };
3689239282AADF06783CBBEA /* AuthFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B3C375FC18002A884A44F1 /* AuthFormatters.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 */; }; A3DFC65C635BE5D96660A432 /* Inputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB95D6804877D2ED244811D0 /* Inputs.swift */; };
A68ED6FBE5307F8674A5A4AB /* HomeView+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CCEF22D356F9EC723999D /* HomeView+Data.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 */; }; 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 */; }; AA0A4F088033729F9B74B2BB /* LegalViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B6E69309F3EDF28E81CC /* LegalViews.swift */; };
ABE383C64BD045B9732AA8C5 /* HomeView+Filtering.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D7ECA55B1FE5FC617E84CB /* HomeView+Filtering.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 */; }; AC7E0F52FEF7976D97190205 /* ApiModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60DC2520DBC74D68AEE659E5 /* ApiModels.swift */; };
@@ -318,7 +318,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
2F56755488E951C453AC2126 /* LCEssentials in Frameworks */, A8DD2855305422C50014CFA9 /* LCEssentials in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -659,7 +659,7 @@
); );
name = PediFoods; name = PediFoods;
packageProductDependencies = ( packageProductDependencies = (
B8D5E8821C95A331C3A19DA1 /* LCEssentials */, A8DD2854305422C50014CFA9 /* LCEssentials */,
); );
productName = PediFoods; productName = PediFoods;
productReference = 7A6D74FD2D2F2F79349EA04E /* PediFoods.app */; productReference = 7A6D74FD2D2F2F79349EA04E /* PediFoods.app */;
@@ -700,7 +700,7 @@
); );
mainGroup = C0C1E2AAD06BC8D9F221FFD5; mainGroup = C0C1E2AAD06BC8D9F221FFD5;
packageReferences = ( packageReferences = (
A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */, A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */,
); );
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@@ -1001,7 +1001,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; 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 = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1046,7 +1046,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; 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 = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1111,7 +1111,7 @@
CLANG_ENABLE_OBJC_WEAK = NO; CLANG_ENABLE_OBJC_WEAK = NO;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
@@ -1242,7 +1242,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
@@ -1314,20 +1314,20 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */ = { A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git"; repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 1.0.11; minimumVersion = 2.0.2;
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
B8D5E8821C95A331C3A19DA1 /* LCEssentials */ = { A8DD2854305422C50014CFA9 /* LCEssentials */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = A90709E0CE6A667BD0C0FF25 /* XCRemoteSwiftPackageReference "LCEssentials" */; package = A8DD2853305422C50014CFA9 /* XCRemoteSwiftPackageReference "LCEssentials" */;
productName = LCEssentials; productName = LCEssentials;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */

View File

@@ -175,12 +175,12 @@ extension ApiClient {
headers: [String: String] headers: [String: String]
) async throws -> String { ) async throws -> String {
let httpMethod = toHTTPMethod(method) 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( return try await API.shared.request(
url: url, url: url,
params: params,
method: httpMethod, method: httpMethod,
body: body,
headers: headers, headers: headers,
jsonEncoding: true,
debug: true debug: true
) )
} }

View File

@@ -345,6 +345,11 @@ extension StoreDetailView {
.disabled(isStoreOpen == false) .disabled(isStoreOpen == false)
.opacity(isStoreOpen ? 1 : 0.65) .opacity(isStoreOpen ? 1 : 0.65)
.offset(x: 7, y: 7) .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) .padding(12)

View File

@@ -13,9 +13,13 @@ final class AuthenticatedProfileNavigationTests: XCTestCase {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout: the standing QA account's Keychain session is
XCUIApplication().logoutIfAuthenticated() // 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 { func testOrdersScreenReachableAndLoadsRealData() throws {
let app = XCUIApplication() let app = XCUIApplication()

View File

@@ -2,17 +2,14 @@ import XCTest
/// Covers a real, authenticated session with the standing QA account (see /// 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). /// 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 /// No per-test logout - the Keychain session is meant to persist across
/// inherit an authenticated Keychain session. /// authenticated-flow tests in the same run; see
/// decisions/2026-09-11-ui-test-shared-login-session.md.
final class AuthenticatedSessionFlowTests: XCTestCase { final class AuthenticatedSessionFlowTests: XCTestCase {
override func setUpWithError() throws { override func setUpWithError() throws {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testLoginReachesAuthenticatedProfile() throws { func testLoginReachesAuthenticatedProfile() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()

View File

@@ -6,18 +6,99 @@ import XCTest
/// so Home shows a real store list - see /// so Home shows a real store list - see
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md.
final class CartCheckoutFlowTests: XCTestCase { final class CartCheckoutFlowTests: XCTestCase {
/// 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/<UDID>/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)")
}
// 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 { $0.label }
.joined(separator: " | ")
signals.append("visible static texts: [\(visibleTexts)]")
return signals.joined(separator: "; ")
}
/// 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 { override func setUpWithError() throws {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md.
XCUIApplication().logoutIfAuthenticated()
}
func testAddProductToCartAndReachCheckout() throws { func testAddProductToCartAndReachCheckout() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
try reachCheckoutWithOneItem(app) try reachCheckoutWithProducts(app)
} }
/// Exercises CheckoutView's payment-method selection and the address /// Exercises CheckoutView's payment-method selection and the address
@@ -27,7 +108,7 @@ final class CartCheckoutFlowTests: XCTestCase {
func testCheckoutPaymentMethodSelectionAndAddressAlterar() throws { func testCheckoutPaymentMethodSelectionAndAddressAlterar() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
try reachCheckoutWithOneItem(app) try reachCheckoutWithProducts(app)
XCTAssertTrue(app.staticTexts["MÉTODO DE PAGAMENTO"].waitForExistence(timeout: 5)) XCTAssertTrue(app.staticTexts["MÉTODO DE PAGAMENTO"].waitForExistence(timeout: 5))
@@ -68,11 +149,10 @@ final class CartCheckoutFlowTests: XCTestCase {
if dismissed == false { if dismissed == false {
app.swipeDown() app.swipeDown()
} }
if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false { XCTAssertTrue(
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" app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10),
try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/after_alterar_dismiss.png")) "Never returned to Checkout. \(quickDiagnostics(app))"
} )
XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout")
// Deliberately not tapping "Confirmar e Pagar" - see doc comment. // Deliberately not tapping "Confirmar e Pagar" - see doc comment.
} }
@@ -87,7 +167,7 @@ final class CartCheckoutFlowTests: XCTestCase {
func testConfirmarEPagarWithCreditCardOpensCardSelectionWithoutSubmitting() throws { func testConfirmarEPagarWithCreditCardOpensCardSelectionWithoutSubmitting() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
try reachCheckoutWithOneItem(app) try reachCheckoutWithProducts(app)
let creditCardRow = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "Cartão de Crédito")).firstMatch 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") XCTAssertTrue(creditCardRow.waitForExistence(timeout: 5), "This store is expected to offer Cartão de Crédito")
@@ -129,7 +209,49 @@ final class CartCheckoutFlowTests: XCTestCase {
app.swipeDown() 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 <price>") 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") XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
// ensureLoggedIn can land on whichever tab it detected the // ensureLoggedIn can land on whichever tab it detected the
@@ -142,33 +264,32 @@ final class CartCheckoutFlowTests: XCTestCase {
// distance + status all combine into its accessibility label), not // distance + status all combine into its accessibility label), not
// a plain static text. // a plain static text.
let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch
if storeCard.waitForExistence(timeout: 15) == false { XCTAssertTrue(
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" storeCard.waitForExistence(timeout: 15),
try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/no_store_card.png")) "Expected store card never appeared on Home. \(quickDiagnostics(app))"
} )
XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home")
storeCard.tap() storeCard.tap()
// A plain (non-addon, non-pizza) product row's "+" control adds // The product row's add button carries an explicit
// directly with no sheet. Once a product's quantity is > 0, its // `.accessibilityIdentifier("storeDetailProductAddButton")`
// outer Button's identifier moves off itself - the same badge- // (StoreDetailView+Components.swift) - added after discovering
// merging bug already fixed for the tab bar's cart icon - because // that matching on the inner "plus" SF Symbol's implicit/
// the row's own quantity Text becomes the Button's accessible // auto-generated identifier was unreliable: `quickDiagnostics`'s
// label/identity instead. The QA account's cart has genuinely // visible-text dump showed a real product ("Alcatra", R$ 14,00)
// accumulated real quantities across many runs today, so // clearly rendered on screen while `images.matching(identifier:
// app.buttons.matching(identifier: "plus") stopped matching once // "plus")` still found nothing, and the same "plus" systemName is
// the first several products all had quantity > 0 (confirmed via // separately reused by CartView's quantity stepper, making it an
// screenshot: the "+" controls were clearly visible on screen // ambiguous identifier to search by in the first place.
// while the buttons-only query found nothing). The nested Image let addButtons = app.buttons.matching(identifier: "storeDetailProductAddButton")
// keeps identifier "plus" regardless of quantity, so target that XCTAssertTrue(
// directly instead - same fix pattern as the cart-tab icon. addButtons.firstMatch.waitForExistence(timeout: 25),
let addButton = app.images.matching(identifier: "plus").firstMatch "Store Detail's product list never loaded. \(quickDiagnostics(app))"
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")) let actualCount = min(count, addButtons.count)
for index in 0..<actualCount {
addProduct(atIndex: index, in: app)
} }
XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded")
addButton.tap()
// Once the cart has items, the tab bar button's accessible label // Once the cart has items, the tab bar button's accessible label
// becomes just the badge count ("4") instead of "Shopping Cart" - // becomes just the badge count ("4") instead of "Shopping Cart" -
@@ -181,7 +302,7 @@ final class CartCheckoutFlowTests: XCTestCase {
XCTAssertTrue(cartTab.waitForExistence(timeout: 5)) XCTAssertTrue(cartTab.waitForExistence(timeout: 5))
cartTab.tap() cartTab.tap()
XCTAssertTrue(app.staticTexts["Meu Carrinho"].waitForExistence(timeout: 10)) XCTAssertTrue(app.staticTexts["Meu Carrinho"].waitForExistence(timeout: 10))
XCTAssertFalse(app.staticTexts["Seu carrinho está vazio"].exists, "Cart still shows empty after adding a product") XCTAssertFalse(app.staticTexts["Seu carrinho está vazio"].exists, "Cart still shows empty after adding products")
app.buttons["Ir para o Pagamento"].tap() app.buttons["Ir para o Pagamento"].tap()
XCTAssertTrue(app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), "Never reached Checkout") XCTAssertTrue(app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), "Never reached Checkout")

View File

@@ -20,9 +20,7 @@ final class HomeFiltersFlowTests: XCTestCase {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md.
XCUIApplication().logoutIfAuthenticated()
}
func testFiltersSheetSelectionsAndApply() throws { func testFiltersSheetSelectionsAndApply() throws {
let app = XCUIApplication() let app = XCUIApplication()

View File

@@ -6,6 +6,24 @@ import XCTest
/// blocked by a separate login-navigation issue documented there and are /// blocked by a separate login-navigation issue documented there and are
/// not covered here. /// not covered here.
final class HomeGuestFlowTests: XCTestCase { 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 { override func setUpWithError() throws {
continueAfterFailure = false continueAfterFailure = false
} }

View File

@@ -9,9 +9,7 @@ final class OrderDetailsFlowTests: XCTestCase {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md.
XCUIApplication().logoutIfAuthenticated()
}
func testOrderDetailsReachableFromOrdersList() throws { func testOrderDetailsReachableFromOrdersList() throws {
let app = XCUIApplication() let app = XCUIApplication()
@@ -23,8 +21,14 @@ final class OrderDetailsFlowTests: XCTestCase {
XCTAssertTrue(app.staticTexts["Meus Pedidos"].waitForExistence(timeout: 10)) XCTAssertTrue(app.staticTexts["Meus Pedidos"].waitForExistence(timeout: 10))
// "Ver Detalhes" appears once per order card; this account has real // "Ver Detalhes" appears once per order card; this account has real
// order history (confirmed in AuthenticatedProfileNavigationTests). // order history (confirmed in AuthenticatedProfileNavigationTests)
let detailsButton = app.buttons["Ver Detalhes"] // 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") XCTAssertTrue(detailsButton.waitForExistence(timeout: 10), "No orders with a 'Ver Detalhes' action found")
detailsButton.tap() detailsButton.tap()

View File

@@ -3,6 +3,25 @@ import XCTest
/// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) and /// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) and
/// the auth intro screen (`LoginView`) it opens. /// the auth intro screen (`LoginView`) it opens.
final class ProfileLoggedOutFlowTests: XCTestCase { 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 { override func setUpWithError() throws {
continueAfterFailure = false continueAfterFailure = false
} }

View File

@@ -9,9 +9,7 @@ final class SavedCardsFlowTests: XCTestCase {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md.
XCUIApplication().logoutIfAuthenticated()
}
func testAddCardFormReachableFromSavedCards() throws { func testAddCardFormReachableFromSavedCards() throws {
let app = XCUIApplication() let app = XCUIApplication()

View File

@@ -7,9 +7,7 @@ final class UserProfileFlowTests: XCTestCase {
continueAfterFailure = false continueAfterFailure = false
} }
override func tearDownWithError() throws { // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md.
XCUIApplication().logoutIfAuthenticated()
}
func testUserProfileReachableFromProfileHeader() throws { func testUserProfileReachableFromProfileHeader() throws {
let app = XCUIApplication() let app = XCUIApplication()

View File

@@ -34,10 +34,38 @@ lane :assemble do |options|
) )
end 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
# "<bundle identifier> 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| lane :beta do |options|
desc "Build and upload to TestFlight" desc "Build and upload to TestFlight"
get_provisioning_profile(api_key_path: "fastlane/apikey.json") renew_provisioning_profiles
assemble assemble
@@ -55,7 +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 # 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_certificates(api_key_path: "fastlane/apikey.json")
get_provisioning_profile(api_key_path: "fastlane/apikey.json") renew_provisioning_profiles
assemble assemble