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:
@@ -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 <<EOF
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "";
|
||||
@@ -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 = 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 = 1;
|
||||
CURRENT_PROJECT_VERSION = 2026.09.11.0.0.1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
|
||||
INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,18 +6,99 @@ 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 {
|
||||
/// 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 {
|
||||
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()
|
||||
app.launch()
|
||||
try reachCheckoutWithOneItem(app)
|
||||
try reachCheckoutWithProducts(app)
|
||||
}
|
||||
|
||||
/// Exercises CheckoutView's payment-method selection and the address
|
||||
@@ -27,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))
|
||||
|
||||
@@ -68,11 +149,10 @@ final class CartCheckoutFlowTests: XCTestCase {
|
||||
if dismissed == false {
|
||||
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"))
|
||||
}
|
||||
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.
|
||||
}
|
||||
@@ -87,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")
|
||||
@@ -129,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 • <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")
|
||||
|
||||
// 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
|
||||
// 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"))
|
||||
}
|
||||
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
|
||||
// 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
|
||||
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"))
|
||||
// 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.
|
||||
let addButtons = app.buttons.matching(identifier: "storeDetailProductAddButton")
|
||||
XCTAssertTrue(
|
||||
addButtons.firstMatch.waitForExistence(timeout: 25),
|
||||
"Store Detail's product list never loaded. \(quickDiagnostics(app))"
|
||||
)
|
||||
|
||||
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
|
||||
// becomes just the badge count ("4") instead of "Shopping Cart" -
|
||||
@@ -181,7 +302,7 @@ final class CartCheckoutFlowTests: XCTestCase {
|
||||
XCTAssertTrue(cartTab.waitForExistence(timeout: 5))
|
||||
cartTab.tap()
|
||||
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()
|
||||
XCTAssertTrue(app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), "Never reached Checkout")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,6 +6,24 @@ 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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -23,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()
|
||||
|
||||
|
||||
@@ -3,6 +3,25 @@ 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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -34,10 +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
|
||||
# "<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|
|
||||
desc "Build and upload to TestFlight"
|
||||
|
||||
get_provisioning_profile(api_key_path: "fastlane/apikey.json")
|
||||
renew_provisioning_profiles
|
||||
|
||||
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
|
||||
#get_certificates(api_key_path: "fastlane/apikey.json")
|
||||
get_provisioning_profile(api_key_path: "fastlane/apikey.json")
|
||||
renew_provisioning_profiles
|
||||
|
||||
assemble
|
||||
|
||||
|
||||
Reference in New Issue
Block a user