--- source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/ui-testing.mdc confidence: 0.9 namespace: work last_synced: 2026-04-28 alwaysApply: false --- # UI Testing Guidelines & Best Practices *This guide covers UI testing practices and patterns specifically for the DuckDuckGo macOS browser.* ## Overview UI Tests verify the end-to-end user experience and interface behavior. They test user workflows, navigation patterns, window management, and complex interactions across the entire application. ## Setting Up UI Tests ### ❗Always Use UITestCase Base Class ```swift class FeatureUITests: UITestCase { // ✅ Use UITestCase, not XCTestCase private var app: XCUIApplication! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication.setUp() // ✅ Use setUp(), never launch() directly // app is already launched and configured by setUp() } } ``` **Why UITestCase is Required**: - Provides proper app lifecycle management - Handles feature flag configuration - Sets up test server environment - Manages window state and cleanup - Provides debugging utilities ### Feature Flag Configuration ```swift // Configure feature flags during test setup override func setUpWithError() throws { app = XCUIApplication.setUp(featureFlags: [ "contextualOnboarding": true, "visualUpdates": false, "duckPlayer": true ]) // Feature flags are automatically applied via FEATURE_FLAGS environment variable } // Alternative: Custom environment app = XCUIApplication.setUp(environment: [ "UITEST_MODE_ONBOARDING": "1" ], featureFlags: [ "newTabPageSections": true ]) ``` #### Privacy Subfeature Configuration ```swift // Configure privacy subfeatures (separate from feature flags) override func setUpWithError() throws { app = XCUIApplication.setUp(privacySubfeatures: [ "autoconsent-filterlist": true, "tracker-allowlist": true ]) // Privacy subfeatures are applied via PRIVACY_SUBFEATURES environment variable } // Combined feature flags and privacy subfeatures app = XCUIApplication.setUp( featureFlags: [ "contextualOnboarding": true ], privacySubfeatures: [ "autoconsent-filterlist": true ] ) ``` **❗Why Feature Flag and Privacy Subfeature Configuration is Critical**: - UI tests run against notarized builds - feature flags can't be changed at runtime - MockFeatureFlagger is NOT available in UI tests (only real DefaultFeatureFlagger) - Feature flags must be configured via FEATURE_FLAGS environment variable before app launch - **Privacy subfeatures are controlled by PrivacyConfiguration, not feature flags** - Privacy subfeatures must be configured via PRIVACY_SUBFEATURES environment variable - Incorrect feature/subfeature state will cause UI tests to fail when expected UI elements don't appear **Key Differences**: - **Feature Flags**: Control app features (e.g., `contextualOnboarding`, `duckPlayer`) - **Privacy Subfeatures**: Control privacy functionality (e.g., `autoconsent-filterlist`, `tracker-allowlist`) - Both use separate environment variables and configuration systems ### File Management in UI Tests The `UITestCase` base class provides built-in file management capabilities for handling downloads, temporary files, and other file operations during testing. **Important**: UI tests run in a sandboxed environment and cannot directly read or delete files from user directories using standard FileManager calls. For non-temp directories, use the `filesToCleanup` pattern that's handled in the base class `tearDown()`. #### Automatic File Cleanup All UI test classes automatically clean up tracked files after test completion: ```swift class DownloadsUITests: UITestCase { func testFileDownload() { let downloadsDir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] let fileName = "test-file.json" let filePath = downloadsDir.appendingPathComponent(fileName).path // Track file for automatic cleanup trackForCleanup(filePath) // Perform download test... // File will be automatically cleaned up after test completes } } ``` #### Reading Files via Local Server Use `readFileViaLocalServer()` to read files that may have permission restrictions: ```swift func testJSONFileContent() throws { let filePath = "/Users/admin/Downloads/test-results.json" // Read file via local test server (bypasses permission issues) let jsonData = try readFileViaLocalServer(filePath: filePath) let results = try JSONDecoder().decode(TestResults.self, from: jsonData) // Validate file contents XCTAssertFalse(results.items.isEmpty) } ``` #### File Management Best Practices ```swift class FileBasedUITests: UITestCase { func testCompleteFileWorkflow() throws { // 1. Track all files that will be created let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-files") trackForCleanup(tempDir.path) let downloadedFile = "/Users/admin/Downloads/results.json" trackForCleanup(downloadedFile) // 2. Perform file operations try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) // 3. Read files via server if needed let fileData = try readFileViaLocalServer(filePath: downloadedFile) // 4. Files are automatically cleaned up in tearDown() } } ``` #### Available Methods - **`trackForCleanup(_ path: String)`**: Track a file/directory for automatic cleanup - **`readFileViaLocalServer(filePath: String) throws -> Data`**: Read file via local test server - **Automatic cleanup**: All tracked files are cleaned up after each test via the base class `tearDown()` ## Element Access Patterns ### Accessibility IDs - The Golden Standard **ALWAYS prefer accessibility IDs for element access**. They provide the most reliable and maintainable element identification. #### Finding Existing Accessibility IDs 1. **Check the actual browser code** for assigned accessibility IDs: ```swift // In browser code - look for patterns like: button.accessibilityIdentifier = "AddressBarViewController.addressBarButton" textField.identifier = "PreferencesGeneralView.switchToNewTabImmediately" ``` 2. **Check XCUIApplication/XCUIElement extensions** for existing quick-accessor variables: ```swift // Check Common/XCUIApplicationExtension.swift extension XCUIApplication { var addressBar: XCUIElement { windows.textFields["AddressBarViewController.addressBarTextField"] } var backButton: XCUIElement { buttons["NavigationBarViewController.backButton"] } } ``` 3. **Use accessibility identifiers consistently**: ```swift // ✅ CORRECT: Using accessibility IDs let addressBar = app.textFields["AddressBarViewController.addressBarTextField"] let bookmarksMenu = app.menuItems["BookmarksMenu.showBookmarks"] let downloadButton = app.buttons["DownloadsViewController.downloadButton"] // ❌ INCORRECT: Using text-based selectors let addressBar = app.textFields["Enter search or URL"] // Fragile - breaks with localization let bookmarksMenu = app.menuItems["Bookmarks"] // Fragile - breaks with text changes ``` #### When to Add New Accessibility IDs **ALWAYS validate with user before modifying main app code** to add missing accessibility IDs: ```swift // Before adding to main app code, ask: // "I need to add accessibility ID 'TabBarViewController.newTabButton' // to the new tab button in TabBarViewController.swift. Should I proceed?" // Then add to the main app: newTabButton.accessibilityIdentifier = "TabBarViewController.newTabButton" ``` #### Element Variable Guidelines **Only create element variables in test cases if they are very specific to the testable area**: ```swift // ✅ CORRECT: Test-specific elements func testSpecificFeatureWorkflow() { let featureSpecificButton = app.buttons["FeatureViewController.specialActionButton"] let uniqueDialog = app.dialogs["FeatureDialog.confirmationDialog"] // Use directly in test } ``` **If elements are generic, add accessors to XCUIApplication/XCUIElement extensions**: ```swift // ✅ CORRECT: Add to XCUIApplicationExtension.swift extension XCUIApplication { var downloadButton: XCUIElement { buttons["DownloadsViewController.downloadButton"] } var preferencesWindow: XCUIElement { windows["PreferencesWindow"] } } // Then use in tests: func testDownloadFlow() { app.downloadButton.click() XCTAssertTrue(app.downloadButton.exists) } ``` ### Timeout Constants Usage **MANDATORY**: Always use `UITests.Timeouts` constants instead of hardcoded timeout values. ```swift // ✅ CORRECT: Use semantic timeout constants XCTAssertTrue(button.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Button should appear") XCTAssertTrue(pageContent.waitForExistence(timeout: UITests.Timeouts.navigation), "Page should load") XCTAssertTrue(localContent.waitForExistence(timeout: UITests.Timeouts.localTestServer), "Local server content should load") // ❌ INCORRECT: Hardcoded timeout values XCTAssertTrue(button.waitForExistence(timeout: 5.0), "Button should appear") XCTAssertTrue(pageContent.waitForExistence(timeout: 30.0), "Page should load") XCTAssertTrue(localContent.waitForExistence(timeout: 15.0), "Local server content should load") ``` **Available Timeout Constants**: - `UITests.Timeouts.elementExistence` (5 sec) - UI elements, buttons, text fields, dialogs - `UITests.Timeouts.navigation` (30 sec) - Page loads, network requests, external sites - `UITests.Timeouts.localTestServer` (15 sec) - Localhost connections, test server content - `UITests.Timeouts.fireAnimation` (30 sec) - Fire animation completion ### Address Bar Validation Rules **MANDATORY**: Always use `app.addressBarValueActivatingIfNeeded()` for address bar validation and prefer exact matches over contains checks. ```swift // ✅ CORRECT: Use helper method with exact match for known URLs XCTAssertEqual(app.addressBarValueActivatingIfNeeded(), "https://example.com/", "Should navigate to example.com") XCTAssertEqual(app.addressBarValueActivatingIfNeeded(), "https://duckduckgo.com/", "Should be on DuckDuckGo") // ❌ INCORRECT: Manual address bar access, partial comparison app.activateAddressBar() let addressBarValue = addressBarTextField.value as? String ?? "" XCTAssertTrue(addressBarValue.contains("example.com"), "Should be on example.com") // ❌ INCORRECT: Contains check for known exact URLs let addressBarValue = app.addressBarValueActivatingIfNeeded() ?? "" XCTAssertTrue(addressBarValue.contains("example.com"), "Should be on example.com") // Use XCTAssertEqual instead ``` **Address Bar Validation Guidelines**: - **Use exact matches** (`XCTAssertEqual`) for known static URLs - **Use contains checks** only for dynamic URLs (search results, localhost with ports) - **Always use** `app.addressBarValueActivatingIfNeeded()` helper method - **Never manually** call `app.activateAddressBar()` + `addressBarTextField.value` ### CRITICAL: XCUIElement Queries Are Always Live **XCUIElement queries are always valid and re-query the UI when accessed (e.g., `exists`, `waitForExistence`). No need to create "fresh" element references:** ```swift // ✅ CORRECT: Reuse the same element reference class FeatureUITests: UITestCase { private var addressBarTextField: XCUIElement! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication.setUp() // Get address bar reference once addressBarTextField = app.addressBar } func testAddressBarNavigation() throws { // Type URL and navigate addressBarTextField.typeText("example.com") addressBarTextField.typeKey(.enter, modifierFlags: []) // Wait for navigation - validate specific content, not generic webView existence let webView = app.webViews.firstMatch let pageContent = webView.staticTexts.containing(NSPredicate(format: "value CONTAINS 'Example Domain'")).firstMatch XCTAssertTrue(pageContent.waitForExistence(timeout: 30.0), "Should navigate to example.com and show page content") // ✅ CORRECT: Reuse original reference - XCUIElement queries are live app.activateAddressBar() // Use helper method instead of manual Cmd+L XCTAssertTrue(addressBarTextField.exists, "Address bar should still be accessible") // ✅ CORRECT: The same element reference works after navigation addressBarTextField.typeText("another-site.com") } } // ❌ INCORRECT: Creating "fresh" element references unnecessarily func testBadPattern() { addressBarTextField.typeText("example.com") // ❌ Wrong: No need to create fresh reference let freshAddressBar = app.textFields["AddressBarViewController.addressBarTextField"] let currentAddressBar = app.textFields["AddressBarViewController.addressBarTextField"] // The original addressBarTextField reference is still valid! } ``` ### Element Access Hierarchy 1. **Accessibility IDs** (most reliable) 2. **Extension-provided accessors** (for common elements) 3. **Stable attributes** (for dynamic content) 4. **Text-based selectors** (last resort, fragile) ## Window and Tab Management ### Essential Window/Tab Validation Patterns Based on our tab navigation testing improvements, always validate both UI state and browser state: #### Single-Window Tab Operations ```swift func testTabOperation() { // Perform action that opens new tab link.click() // ✅ CORRECT: Wait first, then validate counts XCTAssertTrue(app.tabs["New Tab Page"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertEqual(app.windows.count, 1) // Still single window XCTAssertEqual(app.tabs.count, 2) // Original + new tab // Validate both tabs exist XCTAssertTrue(app.tabs["Original Page"].exists) XCTAssertTrue(app.tabs["New Tab Page"].exists) // Validate webview state XCTAssertTrue(app.webViews["New Tab Page"].exists) } ``` #### Multi-Window Operations ```swift func testWindowOperation() { // Perform action that opens new window XCUIElement.perform(withKeyModifiers: [.command, .option]) { link.click() } // ✅ CORRECT: Wait for new window, then validate structure let mainWindow = app.windows.firstMatch let backgroundWindow = app.windows.element(boundBy: 1) XCTAssertTrue(backgroundWindow.waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertEqual(app.windows.count, 2) // Validate content in correct windows XCTAssertTrue(backgroundWindow.webViews["New Window Page"].exists) XCTAssertFalse(mainWindow.webViews["New Window Page"].exists) XCTAssertTrue(mainWindow.webViews["Original Page"].exists) // Validate tab counts per window XCTAssertEqual(mainWindow.tabs.count, 1) XCTAssertEqual(backgroundWindow.tabs.count, 1) XCTAssertTrue(mainWindow.tabs["Original Page"].exists) XCTAssertTrue(backgroundWindow.tabs["New Window Page"].exists) } ``` #### Window Focus Behavior **Important**: When windows are activated/deactivated, the `firstMatch` window changes: ```swift func testWindowActivation() { // Initially: Window A is active (firstMatch), Window B is background let initialActiveWindow = app.windows.firstMatch let backgroundWindow = app.windows.element(boundBy: 1) // Click on background window to activate it backgroundWindow.click() // Now: Window B becomes firstMatch, Window A becomes background let newActiveWindow = app.windows.firstMatch // This is now Window B let newBackgroundWindow = app.windows.element(boundBy: 1) // This is now Window A // Validate the swap occurred XCTAssertNotEqual(initialActiveWindow, newActiveWindow) } ``` ### Navigation Modifier Patterns Understanding modifier key behaviors for comprehensive testing: #### Tab Opening Modifiers - **Command Click**: Opens in background tab - **Command+Shift Click**: Opens in foreground tab (switches to it) - **Middle Click**: Opens in background tab - **Middle+Shift Click**: Opens in foreground tab #### Window Opening Modifiers - **Command+Option Click**: Opens in background window - **Command+Option+Shift Click**: Opens in foreground window (switches to it) - **Middle+Option Click**: Opens in background window - **Middle+Option+Shift Click**: Opens in foreground window #### Validation Patterns by Modifier ```swift // Background tab (Command click) func testCommandClickOpensBackgroundTab() { XCUIElement.perform(withKeyModifiers: [.command]) { link.click() } XCTAssertTrue(app.tabs["New Tab"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertEqual(app.windows.count, 1) XCTAssertEqual(app.tabs.count, 2) // Original page still visible (background tab behavior) XCTAssertTrue(app.webViews["Original Page"].exists) XCTAssertTrue(app.tabs["Original Page"].exists) XCTAssertTrue(app.tabs["New Tab"].exists) } // Foreground tab (Command+Shift click) func testCommandShiftClickOpensActiveTab() { XCUIElement.perform(withKeyModifiers: [.command, .shift]) { link.click() } XCTAssertTrue(app.webViews["New Tab"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertEqual(app.windows.count, 1) XCTAssertEqual(app.tabs.count, 2) // New page visible, original page in background (foreground tab behavior) XCTAssertFalse(app.webViews["Original Page"].exists) XCTAssertTrue(app.tabs["Original Page"].exists) // Tab still exists XCTAssertTrue(app.tabs["New Tab"].exists) } ``` ## Element Interaction Patterns ### Timing and Existence Best Practices **Critical**: Always wait for elements before checking counts or states: ```swift // ❌ BAD: Checking counts before content appears action() XCTAssertEqual(app.windows.count, 1) // Race condition! XCTAssertTrue(element.waitForExistence(...)) // ✅ GOOD: Wait first, then validate action() XCTAssertTrue(element.waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertEqual(app.windows.count, 1) // Now safe to check ``` ### Interaction Methods #### Safe Element Interaction ```swift // ✅ GOOD - Use existing helper methods element.clickAfterExistenceTestSucceeds() element.hoverAfterExistenceTestSucceeds() element.typeURLAfterExistenceTestSucceeds(testURL) // ✅ GOOD - URL handling with colon workaround element.typeURL(url, pressingEnter: true) // Handles colon typing issues element.pasteURL(url, pressingEnter: true) // Faster than typing // ✅ GOOD - Element disappearance tracking element.waitForNonExistence(timeout: UITests.Timeouts.elementExistence) ``` #### Helper Method Organization **CRITICAL: Move common helper methods to `XCUIApplication` extensions instead of duplicating them across test classes:** ```swift // ❌ BAD: Duplicating helper methods across test classes class FeatureUITests: UITestCase { private func setupSingleWindow() { app.typeKey("w", modifierFlags: [.command, .option, .shift]) app.typeKey("n", modifierFlags: .command) } } class AnotherFeatureUITests: UITestCase { private func setupSingleWindow() { // ❌ Duplicate! app.typeKey("w", modifierFlags: [.command, .option, .shift]) app.typeKey("n", modifierFlags: .command) } } // ✅ GOOD: Use existing extension methods or add new ones to XCUIApplicationExtension.swift class FeatureUITests: UITestCase { override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication.setUp() // Use existing extension method app.enforceSingleWindow() // ✅ Already exists in XCUIApplicationExtension.swift } } // ✅ GOOD: Add new helper methods to the extension for reuse extension XCUIApplication { /// Navigate to a URL and wait for page load /// - Parameter url: The URL to navigate to /// - Parameter timeout: Timeout for page load wait /// - Parameter isNewTab: Whether this is happening on a new tab (affects address bar activation) func navigateToURL(_ url: String, timeout: TimeInterval = 10.0, isNewTab: Bool = false) -> Bool { guard addressBar.waitForExistence(timeout: 5.0) else { return false } // Only activate address bar if not on a new tab (new tabs have address bar pre-activated) if !isNewTab { activateAddressBar() } addressBar.typeText(url) addressBar.typeKey(.enter, modifierFlags: []) // Wait for specific content rather than generic webView existence let webView = webViews.firstMatch let pageContent = webView.staticTexts.firstMatch return pageContent.waitForExistence(timeout: timeout) } /// Open downloads popup and verify it appears func openDownloadsPopup() -> Bool { typeKey("j", modifierFlags: [.command]) let downloadsPopup = windows.containing(.any).firstMatch return downloadsPopup.waitForExistence(timeout: 5.0) } } ``` **Available Extension Properties and Methods:** - `app.addressBar` → Address bar text field element (replaces manual `app.textFields["AddressBarViewController.addressBarTextField"]`) - `app.addressBarValueActivatingIfNeeded()` → Activate address bar and return its current value as String? - `app.enforceSingleWindow()` → Close all windows and open new one (replaces `setupSingleWindow()`) - `app.activateAddressBar()` → Activate address bar for input (replaces direct `Cmd+L`) - `app.openNewTab()` → Open new tab via `Cmd+T` - `app.resetBookmarks()` → Reset bookmarks for testing - `app.openBookmarksManager()` → Open bookmarks manager - `app.openBookmarksPanel()` → Show bookmarks panel **Common patterns that should be moved to extensions:** - `setupSingleWindow()` → Use existing `enforceSingleWindow()` - Manual address bar references → Use `app.addressBar` - Direct `Cmd+L` usage → Use `app.activateAddressBar()` - URL navigation helpers → `navigateToURL(_:timeout:)` - Downloads popup helpers → `openDownloadsPopup()` - Common assertion patterns → Extension methods **Benefits of using extension methods:** - ✅ **No duplication** - Write once, use everywhere - ✅ **Consistent behavior** - Same implementation across all tests - ✅ **Easier maintenance** - Fix bugs in one place - ✅ **Better discoverability** - Other developers can find and reuse helpers - ✅ **Cleaner test files** - Focus on test logic, not boilerplate #### Existence Checking Patterns ```swift // ✅ GOOD - Proper existence checking XCTAssertTrue(mainElement.waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertTrue(relatedButton.exists) // Good after waitForExistence passed XCTAssertTrue(anotherComponent.exists) // Good for checking multiple components // ❌ BAD - Avoid these patterns XCTAssertTrue(element.exists) // Without waitForExistence first XCTAssertTrue(element1.waitForExistence(timeout: 5)) XCTAssertTrue(element2.waitForExistence(timeout: 5)) // Consecutive waits slow tests Thread.sleep(forTimeInterval: 2.0) // Unreliable Task.sleep(nanoseconds: 2_000_000_000) // Same issue ``` #### XCUIElement Property Waiting Extensions **NEW: Type-safe property waiting methods for more reliable UI tests:** ```swift // ✅ EXCELLENT: Wait for element properties using key paths let addressBar = app.addressBar let button = app.buttons["TestButton"] // Wait for property to contain substring (case-insensitive) XCTAssertTrue(addressBar.wait(for: \.value, contains: "example.com", timeout: 10.0)) XCTAssertTrue(button.wait(for: \.label, contains: "Submit")) // Wait for property to equal specific value XCTAssertTrue(button.wait(for: \.isEnabled, equals: true, timeout: 5.0)) XCTAssertTrue(addressBar.wait(for: \.value, equals: "https://duckduckgo.com")) // Use in assertions with descriptive failure messages XCTAssertTrue(statusField.wait(for: \.value, equals: "1 of 4"), "Status field should show '1 of 4', but got: \(statusField.value ?? "nil")") ``` **Benefits of property waiting extensions:** - **Type-safe**: Uses Swift key paths instead of string predicates - **Flexible**: Works with any property (\.value, \.label, \.title, \.isEnabled, etc.) - **Reliable**: Built on XCTNSPredicateExpectation for proper waiting - **Debuggable**: Easy to add current value to failure messages #### XCUIElementQuery Filtering Extensions **Type-safe element filtering using key paths:** ```swift // ✅ CORRECT: Filter elements using key paths let webView = app.webViews.firstMatch // Filter by substring (case-insensitive) let pageContent = webView.staticTexts.containing(\.value, containing: "Example Domain").firstMatch let submitButtons = app.buttons.containing(\.label, containing: "Submit") // Filter by exact value let enabledButtons = app.buttons.containing(\.isEnabled, equalTo: true) let specificText = webView.staticTexts.containing(\.value, equalTo: "Welcome").firstMatch let settingsWindow = app.windows.containing(\.title, equalTo: "Settings").firstMatch // Element matching patterns let stopMenuItem = app.menuItems.containing(\.title, equalTo: "Stop").firstMatch let backgroundTab = app.radioButtons.containing(\.title, equalTo: "Background Download").firstMatch // Replace old NSPredicate format strings // ❌ OLD: webView.staticTexts.containing(NSPredicate(format: "value CONTAINS 'Example Domain'")) // ✅ NEW: webView.staticTexts.containing(\.value, containing: "Example Domain") ``` **Available XCUIElementQuery Methods**: - `containing(_:containing:)` - Filter elements where property contains substring - `containing(_:equalTo:)` - Filter elements where property equals value - `containing(_:where:)` - Filter elements containing specific element type with predicate - `matching(_:containing:)` - Alternative filtering method for contains - `matching(_:equalTo:)` - Alternative filtering method for equals - `element(matching:containing:)` - Get single element matching contains criteria - `element(matching:equalTo:)` - Get single element matching equals criteria #### NSPredicate KeyPath Extensions **Type-safe predicate construction for complex filtering:** ```swift // ✅ CORRECT: Using .keyPath() method for predicate construction let webView = app.webViews.firstMatch // Complex element filtering with compound predicates let pdfElement = app.groups.containing(.staticText, where: .keyPath(\.value, beginsWith: "TestPDF")).firstMatch // Compound predicates combining multiple conditions let summaryGroup = webView.groups.containing(.keyPath(\.value, beginsWith: "1p navigation -")).firstMatch let headerGroup = summaryGroup.groups.containing(.staticText, where: .keyPath(\.value, beginsWith: "Blocked")).firstMatch // Advanced predicate construction with chaining let complexPredicate = NSPredicate.keyPath(\.elementType, equalTo: XCUIElement.ElementType.staticText.rawValue) .and(.keyPath(\.value, beginsWith: "Expected")) let pathCell = tables.cells.containing(NSPredicate { element, _ in guard let id = (element as? NSObject)?.value(forKey: #keyPath(XCUIElement.identifier)) as? String, id.hasPrefix("/"), URL(fileURLWithPath: id).standardizedFileURL.path == standardizedPath else { return false } return true }).firstMatch // Window filtering patterns let namedWindow = app.windows.containing(NSPredicate(format: "title == %@", "Page Title")).firstMatch // Replace manual NSPredicate format strings // ❌ OLD: NSPredicate(format: "value CONTAINS %@ AND isEnabled == %@", "text", true) // ✅ NEW: .keyPath(\.value, contains: "text").and(.keyPath(\.isEnabled, equalTo: true)) ``` **Available NSPredicate Static Methods**: **Equality and Membership**: - `.keyPath(_:equalTo:)` - Property equals specific value - `.keyPath(_:in: [values])` - Property in collection of values - `.keyPath(_:in: range)` - Property in numeric range **String Operations**: - `.keyPath(_:contains:)` - Property contains substring (case-insensitive) - `.keyPath(_:like:)` - Pattern matching with wildcards (* and ?) - `.keyPath(_:beginsWith:)` - Property starts with prefix - `.keyPath(_:endsWith:)` - Property ends with suffix - `.keyPath(_:matchingRegex:)` - Property matches regular expression **Numeric Range Operations**: - `.keyPath(_:in: 1...10)` - Closed range (inclusive) - `.keyPath(_:in: 1..<10)` - Half-open range - `.keyPath(_:in: 5...)` - Greater than or equal (>= 5) - `.keyPath(_:in: ..<10)` - Less than (< 10) - `.keyPath(_:in: ...10)` - Less than or equal (<= 10) **Compound Operations**: - `predicate.and(otherPredicate)` - AND combination (instance method) - `predicate.or(otherPredicate)` - OR combination (instance method) - `.and(pred1, pred2, ...)` - AND multiple predicates (static) - `.or(pred1, pred2, ...)` - OR multiple predicates (static) - `predicate.inverted` - NOT predicate (property) **Benefits of NSPredicate KeyPath Extensions**: - **Type Safety**: Compile-time KeyPath validation - **Automatic Format Specifiers**: Handles %@, %d, %f automatically based on type - **Composable**: Easy compound predicate construction with and/or/not - **Reusable**: Store predicates as variables for reuse across tests #### XCUIElementQuery Waiting Extensions **Wait for conditions on element queries (e.g., count changes):** ```swift // ✅ CORRECT: Wait for element count conditions let table = app.tables.firstMatch let cells = table.cells // Wait for exact count XCTAssertTrue(cells.wait(for: \.count, equals: 5, timeout: UITests.Timeouts.localTestServer), "Should have exactly 5 cells") // Wait for range conditions XCTAssertTrue(cells.wait(for: \.count, in: 1...10, timeout: UITests.Timeouts.elementExistence), "Should have 1-10 cells") XCTAssertTrue(cells.wait(for: \.count, in: 2..., timeout: UITests.Timeouts.elementExistence), "Should have at least 2 cells") XCTAssertTrue(cells.wait(for: \.count, in: ..<10, timeout: UITests.Timeouts.elementExistence), "Should have less than 10 cells") // Wait with custom predicate let countPredicate = NSPredicate.keyPath(\.count, in: 1...5) XCTAssertTrue(cells.wait(for: countPredicate, timeout: UITests.Timeouts.elementExistence), "Should have 1-5 cells") // Replace old XCTNSPredicateExpectation patterns // ❌ OLD: Manual XCTNSPredicateExpectation creation // let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "count == %d", 2), object: table.cells) // XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 15.0), .completed) // ✅ NEW: Direct query waiting methods // XCTAssertTrue(table.cells.wait(for: \.count, equals: 2, timeout: UITests.Timeouts.localTestServer)) ``` **Available XCUIElementQuery Wait Methods**: - `wait(for: NSPredicate, timeout:)` - Wait for custom NSPredicate condition - `wait(for: \.count, equals: value, timeout:)` - Wait for count to equal specific value - `wait(for: \.count, in: ClosedRange, timeout:)` - Wait for count in inclusive range (1...10) - `wait(for: \.count, in: Range, timeout:)` - Wait for count in half-open range (1..<10) - `wait(for: \.count, in: PartialRangeFrom, timeout:)` - Wait for count >= value (5...) - `wait(for: \.count, in: PartialRangeUpTo, timeout:)` - Wait for count < value (..<10) - `wait(for: \.count, in: PartialRangeThrough, timeout:)` - Wait for count <= value (...10) **Benefits of query waiting extensions:** - **Type-safe**: Uses Swift key paths with compile-time validation - **Range support**: Native Swift range syntax for numeric conditions - **Simplified**: Replaces verbose XCTNSPredicateExpectation patterns - **Consistent**: Same predicate-based API as filtering methods - **Maintainable**: Compiler catches property and range type errors #### Middle Click Special Handling ```swift // ❌ NEVER: Use synthesized CGEvent approach let mouseDownEvent = CGEvent(mouseEventSource: nil, mouseType: .otherMouseDown, mouseCursorPosition: point, mouseButton: .center)! let mouseUpEvent = CGEvent(mouseEventSource: nil, mouseType: .otherMouseUp, mouseCursorPosition: point, mouseButton: .center)! mouseDownEvent.post(tap: .cghidEventTap) mouseUpEvent.post(tap: .cghidEventTap) // ✅ CORRECT: Use extension method directly element.middleClick() // Extension handles middle-click properly // ✅ CORRECT: For middle-click with modifiers, use separate perform block XCUIElement.perform(withKeyModifiers: [.option]) { element.middleClick() // This doesn't work correctly } ``` ### Test Assertions Must Be Precise **CRITICAL RULE**: All test checks must be predictable and precise. Avoid OR-conditions, Thread.sleep(), and vague checks. ```swift // ❌ WRONG - Vague OR-conditions with sleep Thread.sleep(forTimeInterval: 1.0) let someUIVisible = element1.exists || element2.exists || element3.exists XCTAssertTrue(someUIVisible, "Some UI should be accessible") // ❌ WRONG - Using XCTNSPredicateExpectation for simple element waiting let webView = app.webViews.firstMatch let pageLoaded = XCTNSPredicateExpectation( predicate: NSPredicate(format: "exists == true"), object: webView ) XCTAssertEqual(XCTWaiter.wait(for: [pageLoaded], timeout: 15.0), .completed) // ❌ WRONG - Using 'if' statements for button waiting if runButton.waitForExistence(timeout: 5.0) { runButton.click() } // ✅ CORRECT - Use waitForExistence with assertion for simple element waiting XCTAssertTrue(runButton.waitForExistence(timeout: 15.0), "Run button should be available") runButton.click() // ✅ BEST PRACTICE - Wait for the actual element you need, not its container // Don't wait for webView if you need a button inside it - button existence implies page loaded let runButton = app.webViews.buttons["run"] XCTAssertTrue(runButton.waitForExistence(timeout: 15.0), "Run button should be available") runButton.click() // ✅ CORRECT - Use XCTNSPredicateExpectation only for complex conditions let complexCondition = XCTNSPredicateExpectation( predicate: NSPredicate(format: "count > 2"), object: app.tables.cells ) XCTAssertEqual(XCTWaiter.wait(for: [complexCondition], timeout: 5.0), .completed) ``` **Prohibited Patterns:** - `Thread.sleep()` - Use `waitForExistence` or `XCTNSPredicateExpectation` instead - OR-conditions (`||`) in assertions - Test one specific state - Vague "should be accessible" - Test specific elements and values - Fallback checks - If primary check fails, test should fail clearly - `if button.waitForExistence()` - Use `XCTAssertTrue(button.waitForExistence())` instead - Complex `XCTNSPredicateExpectation` for simple existence checks - Use `waitForExistence` directly #### Address Bar Usage Pattern **Use the extension property and follow activation rules:** ```swift func testAddressBarInteraction() { // ✅ CORRECT: Use extension property let addressBar = app.addressBar // ✅ CORRECT: On new tab page, address bar is already activated - no Cmd+L needed addressBar.pasteURL("example.com") // Wait for navigation let pageContent = webView.staticTexts.containing(NSPredicate(format: "value CONTAINS 'Example Domain'")).firstMatch XCTAssertTrue(pageContent.waitForExistence(timeout: 30.0), "Example.com should load") // ✅ REQUIRED: After navigation, activate address bar before further interaction app.activateAddressBar() // Use extension method instead of direct Cmd+L // Now the address bar is ready for input addressBar.typeText("another-site.com") addressBar.typeKey(.enter, modifierFlags: []) } // ❌ INCORRECT: Don't create your own addressBar reference func testBadPattern() { let addressBarTextField = app.textFields["AddressBarViewController.addressBarTextField"] // ❌ Use app.addressBar instead // ❌ INCORRECT: Don't use Cmd+L on new tab pages app.typeKey("l", modifierFlags: [.command]) // Address bar is already activated on new tabs addressBarTextField.typeText("example.com") } ``` **Address Bar Activation Rules**: - ✅ **NEW TAB PAGE**: Address bar is already activated - do NOT use `Cmd+L` - ✅ **AFTER NAVIGATION**: Address bar becomes read-only - USE `app.activateAddressBar()` before typing - ✅ **USE EXTENSION**: Always use `app.addressBar` instead of creating your own reference - ✅ **USE HELPER METHOD**: Use `app.activateAddressBar()` instead of direct `Cmd+L` #### Element Query Guidelines ```swift // ✅ CORRECT: Use element references efficiently class MyUITests: UITestCase { private var addressBar: XCUIElement! private var webView: XCUIElement! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication.setUp() // Create element references once addressBar = app.textFields["AddressBarViewController.addressBarTextField"] webView = app.webViews.firstMatch } func testNavigation() { // Use the same references throughout the test XCTAssertTrue(addressBar.waitForExistence(timeout: 5.0)) addressBar.typeText("example.com") addressBar.typeKey(.enter, modifierFlags: []) // Wait for specific page content, not generic webView existence let pageContent = webView.staticTexts.containing(NSPredicate(format: "value CONTAINS 'Example Domain'")).firstMatch XCTAssertTrue(pageContent.waitForExistence(timeout: 10.0), "Should show example.com page content") // After navigation, activate address bar for new input app.activateAddressBar() // Same addressBar reference is still valid addressBar.typeText("duckduckgo.com") } } // ❌ INCORRECT: Don't create multiple references to same element func testBadElementHandling() { let addressBar1 = app.textFields["AddressBarViewController.addressBarTextField"] addressBar1.typeText("example.com") // ❌ Unnecessary - addressBar1 is still valid let addressBar2 = app.textFields["AddressBarViewController.addressBarTextField"] let freshAddressBar = app.textFields["AddressBarViewController.addressBarTextField"] // All three references point to the same element! } ``` ### Context Menu Interaction ```swift // Use coordinate-based context menu clicking for reliability across macOS versions func testContextMenuAction() { let webView = app.webViews.firstMatch webView.rightClick() // ✅ Use the coordinate-based context menu hack try app.clickContextMenuItem(matching: { $0.identifier == "PDFContextMenu.print" }) // This method uses coordinate-based clicking instead of direct element interaction // because context menu detection fails on older macOS systems (13/14) in CI } ``` ## Local Test Server Integration UI tests use a local test server running on `http://localhost:8085/` for reliable page loading and content simulation. ### Test Server Architecture - **Port**: 8085 (not 8080) - **Integration**: Uses `TestsURLExtension.swift` shared with Integration Tests - **APIs**: Provides `URL.testsServer` and `.appendingTestParameters()` methods - **Content**: Supports dynamic content generation via query parameters ### Creating Test Content #### Static Content ```swift // Static content from test files let url = URL.testsServer.appendingPathComponent("test-page.html") ``` #### Dynamic Content ```swift // Dynamic content with custom HTML let url = UITests.simpleServedPage(titled: "Test Page") // Creates: http://localhost:8085/?data=...