import XCTest /// Integration tests for document CRUD against the real local backend. /// /// Test Plan IDs: DOC-002, DOC-004, DOC-005 /// Data is seeded via API and cleaned up in tearDown. final class DocumentIntegrationTests: AuthenticatedUITestCase { override var needsAPISession: Bool { true } override var testCredentials: (username: String, password: String) { ("admin", "test1234") } override var apiCredentials: (username: String, password: String) { ("admin", "test1234") } // MARK: - Helpers /// Navigate to the Documents tab and wait for it to load. /// /// The Documents/Warranties view defaults to the Warranties sub-tab and /// shows a horizontal ScrollView for filter chips ("Active Only"). /// Because `pullToRefresh()` uses `app.scrollViews.firstMatch`, it can /// accidentally target that horizontal chip ScrollView instead of the /// vertical content ScrollView, causing the refresh gesture to silently /// fail. Use `pullToRefreshDocuments()` instead of the base-class /// `pullToRefresh()` on this screen. private func navigateToDocumentsAndPrepare() { navigateToDocuments() // Wait for the toolbar add-button (or empty-state / list) to confirm // the Documents screen has loaded. let addButton = app.buttons[AccessibilityIdentifiers.Document.addButton].firstMatch let emptyState = app.otherElements[AccessibilityIdentifiers.Document.emptyStateView] let documentList = app.otherElements[AccessibilityIdentifiers.Document.documentsList] _ = addButton.waitForExistence(timeout: defaultTimeout) || emptyState.waitForExistence(timeout: 3) || documentList.waitForExistence(timeout: 3) } /// Pull-to-refresh on the Documents screen using absolute screen /// coordinates. /// /// The Warranties tab shows a *horizontal* filter-chip ScrollView above /// the content. `app.scrollViews.firstMatch` picks up the filter chips /// instead of the content, so the base-class `pullToRefresh()` silently /// fails. Working with app-level coordinates avoids this ambiguity. private func pullToRefreshDocuments() { // Drag from upper-middle of the screen to lower-middle. // The vertical content area sits roughly between y 0.25 and y 0.90 // of the screen (below the segmented control + search bar + chips). let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.35)) let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.85)) start.press(forDuration: 0.3, thenDragTo: end) // Wait for refresh indicator to appear and disappear let refreshIndicator = app.activityIndicators.firstMatch _ = refreshIndicator.waitForExistence(timeout: 3) _ = refreshIndicator.waitForNonExistence(timeout: defaultTimeout) } /// Pull-to-refresh repeatedly until a target element appears or max retries /// reached. Uses `pullToRefreshDocuments()` which targets the correct /// scroll view on the Documents screen. private func pullToRefreshDocumentsUntilVisible(_ element: XCUIElement, maxRetries: Int = 5) { for _ in 0..