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.
This commit is contained in:
2026-09-11 10:57:48 -03:00
parent 3f99ebcba1
commit 6da8b05cfb
2 changed files with 50 additions and 49 deletions

View File

@@ -24,12 +24,3 @@ jobs:
- name: Run tests with coverage - name: Run tests with coverage
run: fastlane tests 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

View File

@@ -6,31 +6,45 @@ 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 {
/// Plain `print()` from inside a UI test does not reach the CI log on /// Two earlier diagnostic attempts for the timeouts below both failed
/// this runner - fastlane's xcodebuild log formatter only relays /// silently: plain `print()` never reached fastlane's xcodebuild log
/// lines matching its own known patterns (Test Suite/Test Case/etc.) /// (its formatter only relays lines matching its own known patterns),
/// and silently drops anything else, confirmed by pasting the full /// and a file written via `NSTemporaryDirectory()` was never found by
/// log around a timeout and finding no trace of a print()'d marker /// a later CI step's `cat` either - because a UI test target's code
/// string. Writing to a file `cat`'d by a later CI step /// runs as `PediFoodsUITests-Runner`, an app *inside the simulator*,
/// (`.gitea/workflows/test.yml`) works regardless. `NSTemporaryDirectory()` /// sandboxed to that simulator device's own container filesystem;
/// resolves to the CI job's `$TMPDIR`, consistent for the whole job /// `NSTemporaryDirectory()` there resolves to a path under
/// (both the xcodebuild-spawned test process and the later shell step /// `.../CoreSimulator/Devices/<UDID>/data/...`, invisible to the
/// run under the same user session) - unlike the fixed scratch path /// host shell's `$TMPDIR` a later workflow step reads from.
/// this replaced, which was hardcoded to a different machine/session ///
/// and silently no-op'd here. /// A `XCTAssertTrue` failure *message* is the one diagnostic channel
static let diagnosticLogPath = NSTemporaryDirectory() + "pedifoods_uitest_diag.log" /// already confirmed working end to end every single run (the
/// "Store Detail's product list never loaded" text itself always
private static func dumpDiagnostics(_ title: String, app: XCUIApplication) { /// showed up). Its `message` parameter is `@autoclosure`, evaluated
let entry = "\n=== \(title) ===\n\(app.debugDescription)\n" /// only on actual failure - so building a short live-state summary
guard let data = entry.data(using: .utf8) else { return } /// and interpolating it into the message costs nothing when the
if FileManager.default.fileExists(atPath: diagnosticLogPath), /// assertion passes, and needs no file access or CI plumbing at all.
let handle = FileHandle(forWritingAtPath: diagnosticLogPath) { private func quickDiagnostics(_ app: XCUIApplication) -> String {
handle.seekToEndOfFile() var signals: [String] = []
handle.write(data) if app.staticTexts["Loja fechada no momento."].exists {
handle.closeFile() signals.append("closed-store snackbar visible")
} else {
FileManager.default.createFile(atPath: diagnosticLogPath, contents: data)
} }
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 /// 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. /// a known-empty cart instead of an ever-growing one.
override class func setUp() { override class func setUp() {
super.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() let app = XCUIApplication()
app.launch() app.launch()
app.ensureLoggedIn() app.ensureLoggedIn()
@@ -136,10 +146,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(
Self.dumpDiagnostics("Never returned to Checkout after Alterar dismiss", app: app) app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10),
} "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.
} }
@@ -209,10 +219,10 @@ 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(
Self.dumpDiagnostics("No MARIBA store card on Home", app: app) storeCard.waitForExistence(timeout: 15),
} "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 // 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 // keeps identifier "plus" regardless of quantity, so target that
// directly instead - same fix pattern as the cart-tab icon. // directly instead - same fix pattern as the cart-tab icon.
let addButton = app.images.matching(identifier: "plus").firstMatch let addButton = app.images.matching(identifier: "plus").firstMatch
if addButton.waitForExistence(timeout: 25) == false { XCTAssertTrue(
Self.dumpDiagnostics("Store Detail timeout - product list never loaded", app: app) addButton.waitForExistence(timeout: 25),
} "Store Detail's product list never loaded. \(quickDiagnostics(app))"
XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded") )
addButton.tap() 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