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