import XCTest final class CableUITestsScreenshot: XCTestCase { private let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") private enum UIStringKey: String { case addLoad case browseLibrary case library case overviewTab case componentsTab case batteriesTab case chargersTab case close case cancel case settings case defaultLoadName case billOfMaterials case systemEditorTitle case systemsTitle } private let translations: [UIStringKey: [String: String]] = [ .addLoad: [ "en": "Add Load", "de": "Verbraucher hinzufügen", "es": "Añadir carga", "fr": "Ajouter une charge", "nl": "Belasting toevoegen", ], .browseLibrary: [ "en": "Browse Library", "de": "Bibliothek durchsuchen", "es": "Explorar biblioteca", "fr": "Parcourir la bibliothèque", "nl": "Bibliotheek bekijken", ], .library: [ "en": "Library", "de": "Bibliothek", "es": "Biblioteca", "fr": "Bibliothèque", "nl": "Bibliotheek", ], .overviewTab: [ "en": "Overview", "de": "Übersicht", "es": "Resumen", "fr": "Aperçu", "nl": "Overzicht", ], .componentsTab: [ "en": "Components", "de": "Verbraucher", "es": "Componentes", "fr": "Composants", "nl": "Componenten", ], .batteriesTab: [ "en": "Batteries", "de": "Batterien", "es": "Baterías", "fr": "Batteries", "nl": "Batterijen", ], .chargersTab: [ "en": "Chargers", "de": "Ladegeräte", "es": "Cargadores", "fr": "Chargeurs", "nl": "Laders", ], .close: [ "en": "Close", "de": "Schließen", "es": "Cerrar", "fr": "Fermer", "nl": "Sluiten", ], .cancel: [ "en": "Cancel", "de": "Abbrechen", "es": "Cancelar", "fr": "Annuler", "nl": "Annuleren", ], .settings: [ "en": "Settings", "de": "Einstellungen", "es": "Configuración", "fr": "Réglages", "nl": "Instellingen", ], .defaultLoadName: [ "en": "New Load", "de": "Neuer Verbraucher", "es": "Carga nueva", "fr": "Nouvelle charge", "nl": "Nieuwe last", ], .billOfMaterials: [ "en": "Bill of Materials", "de": "Stückliste", "es": "Lista de materiales", "fr": "Liste de matériel", "nl": "Stuklijst", ], .systemEditorTitle: [ "en": "Edit System", "de": "System bearbeiten", "es": "Editar sistema", "fr": "Modifier le système", "nl": "Systeem bewerken", ], .systemsTitle: [ "en": "Systems", "de": "Systeme", "es": "Sistemas", "fr": "Systèmes", "nl": "Systemen", ], ] override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false XCUIDevice.shared.orientation = .portrait } // MARK: - Onboarding Screenshots @MainActor func testOnboardingScreenshots() throws { let app = launchApp(arguments: ["--uitest-reset-data"]) // Wait for Apple Intelligence and other system notifications to appear, then dismiss RunLoop.current.run(until: Date().addingTimeInterval(6)) dismissNotificationBannersIfNeeded() let createSystemButton = app.buttons["create-system-button"] XCTAssertTrue(createSystemButton.waitForExistence(timeout: 8)) dismissNotificationBannersIfNeeded() waitForStability() takeScreenshot(named: "01-OnboardingSystemsView") createSystemButton.tap() let addLoadButton = button(in: app.buttons, for: .addLoad) XCTAssertTrue(addLoadButton.waitForExistence(timeout: 8)) let browseLibraryButton = button(in: app.buttons, for: .browseLibrary) XCTAssertTrue(browseLibraryButton.waitForExistence(timeout: 4)) dismissNotificationBannersIfNeeded() takeScreenshot(named: "02-OnboardingSystemView") browseLibraryButton.tap() let libraryCloseButton = app.buttons["library-view-close-button"] XCTAssertTrue(libraryCloseButton.waitForExistence(timeout: 8)) // Wait for library items AND remote icons to load from PocketBase RunLoop.current.run(until: Date().addingTimeInterval(15)) dismissNotificationBannersIfNeeded() takeScreenshot(named: "04-ComponentSelectorView") libraryCloseButton.tap() XCTAssertTrue(addLoadButton.waitForExistence(timeout: 4)) addLoadButton.tap() let newLoadButton = button(in: app.buttons, for: .defaultLoadName) XCTAssertTrue(newLoadButton.waitForExistence(timeout: 8)) waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "03-LoadEditorView") } // MARK: - Sample Data Screenshots @MainActor func testSampleDataScreenshots() throws { let app = launchAppWithSampleData() dismissNotificationBannersIfNeeded() // Systems list let systemsList = resolvedSystemsList(in: app) XCTAssertTrue(systemsList.waitForExistence(timeout: 4)) dismissNotificationBannersIfNeeded() takeScreenshot(named: "05-SystemsWithSampleData") // Navigate to first system openFirstSystem(in: app, systemsList: systemsList) // Overview tab — wait for navigation animation to complete waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "06-AdventureVanOverview") // Bill of Materials let bomElement = resolveBillOfMaterialsElement(in: app) if !bomElement.waitForExistence(timeout: 6) { bringElementIntoView(bomElement, in: app) } XCTAssertTrue(bomElement.exists) if !bomElement.isHittable { bringElementIntoView(bomElement, in: app, requireHittable: true) } tapElement(bomElement) waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "08-BillOfMaterials") let closeButton = app.buttons["system-bom-close-button"] XCTAssertTrue(closeButton.waitForExistence(timeout: 6)) closeButton.tap() // Components tab tapTab(.componentsTab, in: app) let loadsList = resolvedLoadsList(in: app) XCTAssertTrue(loadsList.waitForExistence(timeout: 4)) dismissNotificationBannersIfNeeded() takeScreenshot(named: "07-AdventureVanLoads") // Open first load → Calculator let firstLoad = loadsList.cells.element(boundBy: 0) XCTAssertTrue(firstLoad.waitForExistence(timeout: 2)) firstLoad.tap() waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "09-AdventureVanCalculator") // Navigate back to system tabs app.navigationBars.buttons.element(boundBy: 0).tap() waitForStability() // Batteries tab tapTab(.batteriesTab, in: app) waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "10-AdventureVanBatteries") // Open first battery → Battery Editor let batteriesList = resolvedBatteriesList(in: app) if batteriesList.waitForExistence(timeout: 4) { let firstBattery = batteriesList.cells.element(boundBy: 0) if firstBattery.waitForExistence(timeout: 2) { firstBattery.tap() waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "11-BatteryEditor") // Navigate back app.navigationBars.buttons.element(boundBy: 0).tap() waitForStability() } } // Chargers tab tapTab(.chargersTab, in: app) waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "12-AdventureVanChargers") // Open first charger → Charger Editor let chargersList = resolvedChargersList(in: app) if chargersList.waitForExistence(timeout: 4) { let firstCharger = chargersList.cells.element(boundBy: 0) if firstCharger.waitForExistence(timeout: 2) { firstCharger.tap() waitForStability() dismissNotificationBannersIfNeeded() takeScreenshot(named: "13-ChargerEditor") } } } // MARK: - App Launch private func launchApp(arguments: [String]) -> XCUIApplication { let app = XCUIApplication() var launchArguments = ["--uitest-reset-data"] launchArguments.append(contentsOf: arguments) app.launchArguments = launchArguments app.launch() return app } private func launchAppWithSampleData() -> XCUIApplication { let app = XCUIApplication() app.launchArguments = ["--uitest-reset-data", "--uitest-sample-data"] app.launch() return app } // MARK: - Navigation Helpers private func openFirstSystem(in app: XCUIApplication, systemsList: XCUIElement) { let firstSystemCell = systemsList.cells.element(boundBy: 0) XCTAssertTrue(firstSystemCell.waitForExistence(timeout: 2)) let systemName = firstSystemCell.staticTexts.firstMatch.label let systemButton = firstSystemCell.buttons.firstMatch if systemButton.exists { systemButton.tap() } else { firstSystemCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() } var detailVisible = waitForSystemDetail(named: systemName, in: app) if !detailVisible { firstSystemCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() detailVisible = waitForSystemDetail(named: systemName, in: app) } XCTAssertTrue(detailVisible) } private func tapTab(_ key: UIStringKey, in app: XCUIApplication) { let identifierMap: [UIStringKey: String] = [ .overviewTab: "overview-tab", .componentsTab: "components-tab", .batteriesTab: "batteries-tab", .chargersTab: "chargers-tab", ] // Use .matching + .firstMatch to avoid "multiple matches" error // with iOS 26 floating tab bar (which creates duplicate elements) if let identifier = identifierMap[key] { let tabButton = app.buttons.matching(identifier: identifier).firstMatch if tabButton.waitForExistence(timeout: 3) { tapElement(tabButton) return } } let tabButton = button(in: app.buttons, for: key) XCTAssertTrue(tabButton.waitForExistence(timeout: 3)) tapElement(tabButton) } private func tapElement(_ element: XCUIElement) { if element.isHittable { element.tap() } else { element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() } } // MARK: - Element Resolution private func resolvedSystemsList(in app: XCUIApplication) -> XCUIElement { let collection = app.collectionViews["systems-list"] if collection.waitForExistence(timeout: 6) { return collection } if app.collectionViews.firstMatch.waitForExistence(timeout: 2) { return app.collectionViews.firstMatch } let table = app.tables["systems-list"] if table.waitForExistence(timeout: 6) { return table } XCTAssertTrue(app.tables.firstMatch.waitForExistence(timeout: 2)) return app.tables.firstMatch } private func resolvedLoadsList(in app: XCUIApplication) -> XCUIElement { let collection = app.collectionViews["loads-list"] if collection.waitForExistence(timeout: 6) { return collection } let table = app.tables["loads-list"] XCTAssertTrue(table.waitForExistence(timeout: 6)) return table } private func resolvedBatteriesList(in app: XCUIApplication) -> XCUIElement { let collection = app.collectionViews["batteries-list"] if collection.waitForExistence(timeout: 6) { return collection } let table = app.tables["batteries-list"] if table.waitForExistence(timeout: 6) { return table } // Fallback: any list on screen if app.collectionViews.firstMatch.waitForExistence(timeout: 2) { return app.collectionViews.firstMatch } return app.tables.firstMatch } private func resolvedChargersList(in app: XCUIApplication) -> XCUIElement { let collection = app.collectionViews["chargers-list"] if collection.waitForExistence(timeout: 6) { return collection } let table = app.tables["chargers-list"] if table.waitForExistence(timeout: 6) { return table } if app.collectionViews.firstMatch.waitForExistence(timeout: 2) { return app.collectionViews.firstMatch } return app.tables.firstMatch } private func resolveBillOfMaterialsElement(in app: XCUIApplication) -> XCUIElement { let identifier = "system-bom-button" let buttonByIdentifier = app.buttons.matching(identifier: identifier).firstMatch if buttonByIdentifier.exists { return buttonByIdentifier } let elementByIdentifier = app.otherElements.matching(identifier: identifier).firstMatch if elementByIdentifier.exists { return elementByIdentifier } let candidates = candidateStrings(for: .billOfMaterials) for candidate in candidates { let button = app.buttons[candidate] if button.exists { return button } let other = app.otherElements[candidate] if other.exists { return other } } return buttonByIdentifier } // MARK: - Screenshots & Stability private func takeScreenshot(named name: String) { let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.name = name attachment.lifetime = .keepAlways add(attachment) } private func waitForStability(long: Bool = false) { RunLoop.current.run(until: Date().addingTimeInterval(long ? 3.0 : 0.5)) } private func waitForSystemDetail(named systemName: String, in app: XCUIApplication, timeout: TimeInterval = 6) -> Bool { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if app.otherElements["system-overview"].exists { return true } let navBar = app.navigationBars.firstMatch if navBar.buttons[systemName].exists || navBar.staticTexts[systemName].exists { return true } RunLoop.current.run(until: Date().addingTimeInterval(0.25)) } return app.otherElements["system-overview"].exists } private func bringElementIntoView( _ element: XCUIElement, in app: XCUIApplication, requireHittable: Bool = false, attempts: Int = 8 ) { let scrollContainer = app.scrollViews.firstMatch.exists ? app.scrollViews.firstMatch : app.tables.firstMatch for _ in 0.. [String] { var values = Set() if let languageCode = Locale.preferredLanguages.first.flatMap({ String($0.prefix(2)) }), let localized = translations[key]?[languageCode] { values.insert(localized) } if let english = translations[key]?["en"] { values.insert(english) } if let others = translations[key]?.values { values.formUnion(others) } return Array(values) } private func button(in query: XCUIElementQuery, for key: UIStringKey) -> XCUIElement { let candidates = candidateStrings(for: key) for candidate in candidates { let element = query[candidate] if element.exists { return element } } let predicate = NSPredicate( format: "label IN %@ OR identifier IN %@", NSArray(array: candidates), NSArray(array: candidates) ) return query.matching(predicate).firstMatch } }