import XCTest /// Base class for all page objects providing common waiting and assertion utilities. /// /// Replaces ad-hoc `sleep()` calls with condition-based waits for reliable, /// non-flaky UI tests. All screen page objects should inherit from this class. class BaseScreen { let app: XCUIApplication let timeout: TimeInterval init(app: XCUIApplication, timeout: TimeInterval = 10) { self.app = app self.timeout = timeout } // MARK: - Wait Helpers (replaces fixed sleeps) /// Waits for an element to exist within the timeout period. /// Fails the test with a descriptive message if the element does not appear. @discardableResult func waitForElement(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement { let t = timeout ?? self.timeout XCTAssertTrue(element.waitForExistence(timeout: t), "Element \(element) did not appear within \(t)s") return element } /// Waits for an element to disappear within the timeout period. /// Fails the test if the element is still present after the timeout. func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval? = nil) { let t = timeout ?? self.timeout let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) let result = XCTWaiter().wait(for: [expectation], timeout: t) XCTAssertEqual(result, .completed, "Element \(element) did not disappear within \(t)s") } /// Waits for an element to become hittable (visible and interactable). /// Returns the element for chaining. @discardableResult func waitForHittable(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement { let t = timeout ?? self.timeout let predicate = NSPredicate(format: "isHittable == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) _ = XCTWaiter().wait(for: [expectation], timeout: t) return element } // MARK: - State Assertions /// Asserts that an element with the given accessibility identifier exists. func assertExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) { let element = app.descendants(matching: .any)[identifier] XCTAssertTrue(element.waitForExistence(timeout: timeout), "Element '\(identifier)' not found", file: file, line: line) } /// Asserts that an element with the given accessibility identifier does not exist. func assertNotExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) { let element = app.descendants(matching: .any)[identifier] XCTAssertFalse(element.exists, "Element '\(identifier)' should not exist", file: file, line: line) } // MARK: - Navigation /// Taps the first button in the navigation bar (typically the back button). func tapBackButton() { app.navigationBars.buttons.element(boundBy: 0).tap() } /// Subclasses must override this property to indicate whether the screen is currently displayed. var isDisplayed: Bool { fatalError("Subclasses must override isDisplayed") } }