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.
This commit is contained in:
@@ -24,3 +24,12 @@ 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
|
||||||
|
|||||||
@@ -6,6 +6,33 @@ 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
|
||||||
|
/// 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
|
/// The cart is on-device state (`CartState`/`SessionStateStore`, see
|
||||||
/// `AppState.swift`), not server-side - it persists across app
|
/// `AppState.swift`), not server-side - it persists across app
|
||||||
/// relaunches on this simulator just like the login session (see
|
/// 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.
|
/// 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()
|
||||||
@@ -106,8 +137,7 @@ final class CartCheckoutFlowTests: XCTestCase {
|
|||||||
app.swipeDown()
|
app.swipeDown()
|
||||||
}
|
}
|
||||||
if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false {
|
if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false {
|
||||||
print("=== Never returned to Checkout after Alterar dismiss - accessibility hierarchy ===")
|
Self.dumpDiagnostics("Never returned to Checkout after Alterar dismiss", app: app)
|
||||||
print(app.debugDescription)
|
|
||||||
}
|
}
|
||||||
XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout")
|
XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout")
|
||||||
|
|
||||||
@@ -180,8 +210,7 @@ final class CartCheckoutFlowTests: XCTestCase {
|
|||||||
// 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 {
|
if storeCard.waitForExistence(timeout: 15) == false {
|
||||||
print("=== No MARIBA store card on Home - accessibility hierarchy ===")
|
Self.dumpDiagnostics("No MARIBA store card on Home", app: app)
|
||||||
print(app.debugDescription)
|
|
||||||
}
|
}
|
||||||
XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home")
|
XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home")
|
||||||
storeCard.tap()
|
storeCard.tap()
|
||||||
@@ -201,15 +230,7 @@ final class CartCheckoutFlowTests: XCTestCase {
|
|||||||
// 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 {
|
if addButton.waitForExistence(timeout: 25) == false {
|
||||||
// A hardcoded scratch-path screenshot write here was silently
|
Self.dumpDiagnostics("Store Detail timeout - product list never loaded", app: app)
|
||||||
// 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")
|
XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded")
|
||||||
addButton.tap()
|
addButton.tap()
|
||||||
|
|||||||
Reference in New Issue
Block a user