Files

53 KiB

source, confidence, namespace, last_synced, alwaysApply
source confidence namespace last_synced alwaysApply
~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/testing.mdc 0.9 work 2026-04-28 false

Testing Guidelines & Best Practices

This guide covers testing practices and patterns for the DuckDuckGo browser on iOS and macOS platforms.

🚨 MANDATORY: Testing Execution Rules

NEVER Run Tests Without Permission

NEVER execute any test commands without EXPLICIT user permission or unles user explicitly asked to in their prompt.

Required Testing Workflow:

  1. Write or modify test code as requested
  2. if user did not ask to run tests in their prompt, STOP before running any test commands:
    • swift test
    • npm test
    • xcodebuild test
    • fastlane test
    • Any other test execution commands
  3. ASK the user: "Should I run the tests?"
  4. WAIT for explicit permission (e.g., "yes", "run tests", "test it")
  5. Only then execute test commands

This rule applies to ALL test execution - unit tests, integration tests, UI tests, performance tests, etc.


Future Improvements

This guide is a living document. Consider these areas for future improvements:

  • Tab Extensions Testing: Expand patterns for testing complex tab extension interactions and lifecycle management
  • WebKit Integration Testing: Add comprehensive patterns for testing WKWebView configurations, user scripts, and content blocking integration
  • Privacy Feature Testing: Develop specialized testing approaches for tracker protection, HTTPS upgrade, and content blocking rule validation
  • Cross-Platform Testing: Create patterns for testing SharedPackages functionality across iOS and macOS with consistent behavior validation
  • Fire Button Integration Testing: Add comprehensive testing patterns for data clearing workflows across all browser components
  • Autofill and Credential Testing: Expand testing approaches for AutofillCredentialProvider, password management, and form filling scenarios
  • Sync Testing: Develop patterns for testing bookmark sync, conflict resolution, and cross-device data consistency
  • AI Chat Integration Testing: Add testing patterns for AI chat functionality, context management, and user interaction flows
  • Feature Flag Testing: Expand MockFeatureFlagger usage patterns and integration testing with real feature configurations

Unit Tests

What to Include

Unit tests should focus on testing individual components, functions, or classes in isolation. They should be:

  • Fast: Run quickly (< 1 second per test)
  • Independent: Not depend on external systems or other tests
  • Deterministic: Always produce the same result given the same input
  • Focused: Test one specific behavior or functionality

When to Write Unit Tests

Model Logic

Testing business logic, data transformations, and model behavior:

func testBookmarkFolderCreation() {
    let folder = BookmarkFolder(title: "Test Folder")
    XCTAssertEqual(folder.title, "Test Folder")
    XCTAssertTrue(folder.children.isEmpty)
}

Algorithms/Parsers

Testing parsing logic, URL manipulation, search algorithms:

func testURLSchemeDetection() {
    let detector = URLSchemeDetector()
    XCTAssertTrue(detector.isValidURL("https://duckduckgo.com"))
    XCTAssertFalse(detector.isValidURL("invalid-url"))
}

Utility Functions

Testing helper functions, extensions, formatters:

func testDateFormatter() {
    let formatter = DateFormatter.shortDate
    let date = Date(timeIntervalSince1970: 1640995200) // 2022-01-01
    XCTAssertEqual(formatter.string(from: date), "1/1/22")
}

State Management

Testing ViewModels, state transitions, and data flow:

func testViewModelStateTransition() {
    let viewModel = SearchViewModel()
    viewModel.performSearch("test query")
    XCTAssertEqual(viewModel.state, .loading)
}

What to Avoid

Simple Property Toggles

Testing trivial getters/setters:

// ❌ DON'T test this
func testIsEnabledToggle() {
    feature.isEnabled = true
    XCTAssertTrue(feature.isEnabled)
}

Complex UI Interactions

Use Integration or UI tests instead.

External Dependencies

File system, network calls, databases.

State/Strategy Pattern Switching

These are better suited for integration tests:

// ❌ DON'T test state switching in unit tests
func testStateSwitching() {
    stateMachine.transition(to: .loading)
    stateMachine.transition(to: .loaded)
    // This is brittle and doesn't test real behavior
}

Mocks and Test Helpers

The DuckDuckGo browser project includes multiple mock categories for testing different components and scenarios:

Mock Categories

Unit Tests Mocks

For testing individual components in isolation:

  • UI mocks: MockWindow, MockTabViewItemDelegate
  • WebView mocks: WebViewMock, WKSecurityOriginMock
  • Storage mocks: FileStoreMock, UserDefaultsMock
  • Feature-specific mocks: MockBookmarkManager, MockFireproofDomains, MockAIChatPreferencesStorage

Integration Tests Mocks

For testing component interactions and workflows:

  • Content blocking mocks
  • Tab navigation mocks
  • Fire integration mocks
  • Onboarding flow mocks
  • System integration mocks

BSK Tests Mocks

For testing BrowserServicesKit functionality:

  • Feature flag mocks: MockFeatureFlagger
  • Privacy configuration mocks: MockPrivacyConfiguration
  • Statistics mocks: MockStatisticsStore
  • Variant management mocks: MockVariantManager
  • Network mocks: MockAPIService

Mock Usage Examples

UI Testing with MockWindow

func testViewControllerLifecycle() {
    autoreleasepool {
        let mockWindow = MockWindow()
        let viewController = MyViewController()
        
        mockWindow.contentViewController = viewController
        viewController.viewDidLoad() // Safe to call with MockWindow
        
        XCTAssertNotNil(viewController.view)
    } // Ensures proper cleanup
}

WebKit Testing with WebViewMock

func testGeolocationPermission() {
    autoreleasepool {
        let mockWebView = WebViewMock()
        let origin = WKSecurityOriginMock.new(url: URL(string: "https://example.com")!)
        let frameInfo = WKFrameInfoMock(webView: mockWebView, securityOrigin: origin, 
                                       request: URLRequest(url: URL(string: "https://example.com")!), 
                                       isMainFrame: true)
        
        // Test permission handling without actual web content
        permissionManager.requestGeolocationPermission(for: frameInfo) { granted in
            XCTAssertTrue(granted)
        }
    }
}

Feature Flag Testing with MockFeatureFlagger

⚠️ CRITICAL: MockFeatureFlagger is REQUIRED for feature tests throughout the entire codebase (iOS, macOS, BSK). Tests will crash without it due to DefaultFeatureFlagger assertions that prevent real feature flag usage in test environments.

func testFeatureFlaggedBehavior() {
    let mockFlagger = MockFeatureFlagger()
    mockFlagger.enabledFeatureFlags = [.contextualOnboarding]
    
    let feature = SomeFeature(featureFlagger: mockFlagger)
    XCTAssertTrue(feature.isOnboardingEnabled)
}

// Alternative approach using featuresStub
func testWithFeatureStub() {
    let mockFlagger = MockFeatureFlagger()
    mockFlagger.featuresStub = [
        "contextualOnboarding": true,
        "duckPlayer": false
    ]
    
    let component = TestComponent(featureFlagger: mockFlagger)
    XCTAssertTrue(component.hasOnboarding)
    XCTAssertFalse(component.hasDuckPlayer)
}

// Testing feature flag injection into Tab
func testTabWithFeatureFlags() {
    autoreleasepool {
        let mockFlagger = MockFeatureFlagger()
        mockFlagger.enabledFeatureFlags = [.tabCrashDebugging]
        
        let tab = Tab(content: .newtab, featureFlagger: mockFlagger)
        XCTAssertTrue(tab.canKillWebContentProcess)
    }
}

Why MockFeatureFlagger is Essential:

  • Test Isolation: Prevents real feature flag configurations from affecting test outcomes
  • Crash Prevention: DefaultFeatureFlagger includes assertions that crash tests if not using MockFeatureFlagger
  • Controlled Environment: Ensures predictable test behavior regardless of remote configuration
  • Performance: Avoids network calls and complex privacy configuration setup

Mock Guidelines

When to Use Each Mock Category

  • Unit Tests Mocks: Testing individual classes, methods, and components in isolation
  • Integration Tests Mocks: Testing workflows, component interactions, and system behavior
  • BSK Tests Mocks: Testing shared functionality, feature flags, and cross-platform components

Memory Management with Mocks

  • Always use autoreleasepool {} for UI and WebKit mocks
  • Use shared mock instances when testing multiple scenarios
  • Reset mock state between tests to prevent pollution
  • Be careful with reactive mocks that use Combine publishers

Mock Selection Criteria

When creating mocks:

  • Check existing mocks first - Use available mocks before creating new ones
  • Create what you need - Don't hesitate to create mocks for single-use scenarios if no suitable mock exists
  • Focus on behavior - Mock the interfaces and protocols your code depends on
  • Make them realistic - Mocks should behave similarly to real implementations

Avoid "Testing Mocks" (Anti-patterns)

Golden Rule: "If you can omit mocking, omit mocking" - The more you mock, the more you deviate from the real system, decreasing test confidence.

Testing Implementation Details Instead of Behavior

// ❌ BAD - Testing that internal methods are called
func testUserServiceCallsCorrectMethods() {
    let mockRepository = MockUserRepository()
    let userService = UserService(repository: mockRepository)
    
    userService.getCurrentUser()
    
    XCTAssertTrue(mockRepository.lookupSessionsCalled)  // Testing HOW, not WHAT
    XCTAssertTrue(mockRepository.getLatestSessionCalled)
}

// ✅ GOOD - Testing the actual outcome
func testUserServiceReturnsCurrentUser() {
    let mockRepository = MockUserRepository()
    mockRepository.mockUser = User(id: "123", name: "John")
    let userService = UserService(repository: mockRepository)
    
    let user = userService.getCurrentUser()
    
    XCTAssertEqual(user?.name, "John")  // Testing WHAT happens, not HOW
}

Over-mocking Creates a Fake System

// ❌ BAD - Testing mostly mock interactions
func testCompleteWorkflow() {
    let mockAuth = MockAuthService()
    let mockAPI = MockAPIService() 
    let mockStorage = MockStorage()
    let mockLogger = MockLogger()
    let mockAnalytics = MockAnalytics()
    // ... testing interactions between mocks, not real behavior
}

Complex Scenarios Need Integration Testing

// ❌ BAD - Complex mocking setup for user flows
func testBookmarkSyncFlow() {
    let mockNetworkMonitor = MockNetworkMonitor()
    let mockSyncService = MockSyncService()
    let mockBookmarkStore = MockBookmarkStore()
    let mockConflictResolver = MockConflictResolver()
    // ... 50 lines of mock setup for a flow that should be integration tested
}

// ✅ BETTER - Use Integration Test instead
// This complex scenario is better tested as an integration test
// with real components, providing higher confidence and less maintenance

When complex mocking suggests better alternatives:

  • If you're setting up many interconnected mocks → Consider an Integration Test with real components
  • If you're mocking user interaction flows → Consider a UI Test that exercises the actual user journey
  • If mock setup takes longer than the test logic → You're probably over-mocking

Memory Management - TestRunHelper

The project includes TestRunHelper that provides automatic memory management and tracking during testing.

TestRunHelper Features (Automatically Enabled)

  • Autorelease Tracking: Automatically enabled for all tests, tracks specific classes (WKWebView, NSWindow, NSWindowController, NSViewController, TabBarItemCellView)
  • View Hierarchy Tracking: Monitors NSView creations and validates deallocation with 3-second timeout
  • UI Presentation Prevention: Fails tests that try to present real UI with fatalError("Unit Tests should not present UI. Use MockWindow if needed.")
  • Test Instance Variables Clearing: Monitors test case instance variables and ensures they're cleared after test completion
  • Shared Resource Management: Shared WKProcessPool for WebKit tests, temporary directory cleanup

Memory Issue Detection

TestRunHelper will break/assert in these scenarios:

  • CI Environment: fatalError() with test failure
  • Local Development: breakByRaisingSigInt() with debugging guidance
  • Tab Deallocation Check: Tab.deinit includes comprehensive WebKit object deallocation verification

Override allowedNonNilVariables for Legitimate Cases

override var allowedNonNilVariables: Set<String> {
    ["constantData", "sharedManager", "staticConfiguration"]
}

When to Use autoreleasepool

Always Required:

// 1. View Controller lifecycle testing
func testViewControllerLifecycle() {
    autoreleasepool {
        let window = MockWindow()
        let viewController = MyViewController()
        window.contentViewController = viewController
        // Test lifecycle methods
    } // Ensures immediate cleanup
}

// 2. Heavy object creation in loops
func testBatchOperations() {
    for i in 0..<1000 {
        autoreleasepool {
            let object = HeavyObject(data: testData[i])
            processObject(object)
        } // Releases object immediately instead of waiting for test end
    }
}

// 3. WebKit and content blocking tests
func testContentBlockingRules() {
    autoreleasepool {
        let rulesManager = MockContentBlockerRulesManager()
        let webView = WebViewMock()
        // Test content blocking behavior
    } // WebKit objects can be memory intensive
}

// 4. File system and database operations
func testDatabaseOperations() {
    autoreleasepool {
        let context = managedObjectContext
        let objects = createTestObjects(count: 100)
        context.save()
    } // Ensures Core Data cleanup
}

Memory Management Best Practices

  • Use autoreleasepool when creating more than a few objects in a test
  • Always wrap view controller tests that involve UI hierarchy
  • Use for any test involving file I/O, network operations, or large data
  • Wrap tearDown operations when dealing with complex object hierarchies
  • Essential for batch operations and performance tests

Template for Memory-Safe Testing

func testFeature() {
    autoreleasepool {
        // 1. Setup mocks and dependencies
        let mockWindow = MockWindow()
        let mockManager = MockSomeManager()
        
        // 2. Create objects under test
        let objectUnderTest = MyObject(dependencies: mockManager)
        
        // 3. Perform test operations
        objectUnderTest.performAction()
        
        // 4. Assert results
        XCTAssertEqual(objectUnderTest.state, .expected)
        
        // 5. Objects are automatically released at end of autoreleasepool
    }
}

AutoreleaseTracker

What it is: AutoreleaseTracker creates tracker objects that are autoreleased alongside specific tracked objects, maintaining malloc stack traces for debugging memory leaks.

What it tracks: WKWebView, NSWindow, NSWindowController, NSViewController, TabBarItemCellView

How to debug autorelease issues:

  1. Enable MallocStackLogging in Tests scheme → Arguments → Environment Variables
  2. Run your test and wait for deallocation timeout
  3. Open Memory Browser in Xcode (Debug → Debug Workflow → View Memory)
  4. Search for "AutoreleaseTracker" in Memory Browser
  5. Analyze the stack trace to see where the autorelease call was made

Integration Tests

What to Include

Integration tests verify that multiple components work together correctly. They test:

  • Component Interactions: How different modules communicate
  • Data Flow: End-to-end data processing
  • System Integration: External services, APIs, databases

When to Write Integration Tests

Complex Feature Workflows

Testing complete user journeys:

func testBookmarkSyncFlow() {
    // Test bookmark creation -> sync -> retrieval
    let bookmark = createBookmark()
    syncManager.sync()
    let retrievedBookmark = bookmarkStore.fetch(by: bookmark.id)
    XCTAssertEqual(bookmark.title, retrievedBookmark?.title)
}

State Management

Testing complex state transitions in real scenarios.

Cross-Module Communication

Testing how different packages interact.

Database Operations

Testing Core Data models and persistence.

Integration Test Anti-patterns

  • Debug Information: Don't include debug prints, use Logger.tests instead
  • Real UI Dependencies: Prefer mock windows and views
  • Heavy Resource Usage: Avoid tests that load unnecessary resources
  • External Network Calls: Use mocked services

Best Practices for Integration Tests

class IntegrationTestCase: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Use mock window to avoid UI dependencies
        let mockWindow = MockWindow()
        setupTestEnvironment(window: mockWindow)
    }
    
    override func tearDown() {
        // Ensure all resources are properly released
        cleanupTestEnvironment()
        super.tearDown()
    }
    
    func testFeatureIntegration() {
        // Test real component interaction without UI
        let coordinator = FeatureCoordinator()
        let result = coordinator.performAction()
        XCTAssertNotNil(result)
    }
}

BSK Tests

BSK (BrowserServicesKit) tests are specialized for the shared browser services package.

Logging in BSK Tests

Getting BSK Test Logs

From GitHub Actions: Download the bsk-xctest-log-stream.log artifact from failed builds

Local Development:

# Use real-time log streaming (in separate terminal)
log stream --debug --info --predicate 'process == "xctest"' --style syslog &
swift test

# For Swift Testing framework tests (using @Test macro):
swift test --enable-swift-testing

# For specific test target with Swift Testing:
swift test --filter PersistenceTests --enable-swift-testing

⚠️ AI Assistant Note: These commands are examples for manual execution only. Never run test commands automatically without explicit user permission.

Note: Use --enable-swift-testing flag when running tests that use the Swift Testing framework (@Test macro) instead of XCTest. This enables better test output and proper test discovery for Swift Testing tests.

Enabling Private Data in Console App

By default, macOS Console app shows <private> instead of actual values for privacy. To see private data for debugging:

  1. Install the logging plist (located at SharedPackages/BrowserServicesKit/com.apple.system.logging.plist):

    sudo cp com.apple.system.logging.plist /Library/Preferences/Logging/com.apple.system.logging.plist
    
  2. Restart Console app to apply changes

  3. Private data will now be visible in system logs for debugging purposes

Note: Only use this for debugging - remove the plist when done to restore privacy protection.

Tab Extensions Testing

Tab Extensions are modular components that provide specific functionality to tabs. For comprehensive guidance on Tab Extensions architecture and implementation patterns, see the Tab Extensions Knowledge Sharing.

Key Files to Reference

  • TabExtensions.swift - Extension registration and architecture patterns
  • TabExtensionsBuilder.swift - Extension initialization and dependency injection
  • Tab+Navigation.swift - setupNavigationDelegate for navigation handling

Basic Tab Extension Testing

class MyTabExtensionTests: XCTestCase {
    var extensionsBuilder: TestTabExtensionsBuilder!
    
    override func setUp() {
        super.setUp()
        // Load only the extension you want to test
        extensionsBuilder = TestTabExtensionsBuilder(load: [MyTabExtension.self])
    }
    
    func testExtensionFunctionality() {
        autoreleasepool {
            let tab = Tab(content: .none, extensionsBuilder: extensionsBuilder)
            let myExtension = tab.extensions.myExtension
            
            XCTAssertNotNil(myExtension)
            myExtension?.performAction()
            XCTAssertEqual(myExtension?.publicProperty, "expected_value")
        }
    }
}

Advanced Testing with Mocked Dependencies

class AdClickAttributionTabExtensionTests: XCTestCase {
    func testExtensionWithMockedDependencies() {
        let extensionsBuilder = TestTabExtensionsBuilder(load: [AdClickAttributionTabExtension.self]) { builder in { args, dependencies in
            builder.override {
                AdClickAttributionTabExtension(
                    // Override with mocked dependencies
                    userContentControllerFuture: Future { $0(.success(self.mockUserContentController)) },
                    dependencies: dependencies.privacyFeatures.contentBlocking
                ) { _ in (logic: self.mockLogic, detection: self.mockDetection) }
            }
        }}
        
        autoreleasepool {
            let tab = Tab(content: .none, extensionsBuilder: extensionsBuilder)
            // Test extension behavior with mocked dependencies
        }
    }
}

Tab Extension Testing Guidelines

Do:

  • Use TestTabExtensionsBuilder for controlled extension loading
  • Load only required extensions for isolated testing
  • Test through public protocol interfaces
  • Use autoreleasepool for memory management
  • Mock dependencies through override patterns

Don't:

  • Load all extensions unless testing integration
  • Access private implementation details
  • Create real UI (use MockWindow)
  • Ignore proper setup and teardown

UI Tests

For comprehensive UI testing guidelines, patterns, and best practices specifically for the DuckDuckGo macOS browser, see UI Testing Guidelines & Best Practices.

The UI testing documentation covers:

  • Element Access Patterns: Accessibility IDs, element variables, extension usage
  • Window and Tab Management: Multi-window operations, navigation modifiers, validation patterns
  • Element Interaction: Timing best practices, middle-click handling, context menus
  • Test Server Integration: Local test server setup and content creation
  • Performance Optimizations: Pasteboard usage, avoiding slow operations
  • Build Architecture: Compatibility requirements across macOS versions
  • Advanced Patterns: Tab extensions, settings testing, bookmark/history workflows

Snapshot Testing

Overview

The project uses swift-snapshot-testing for snapshot testing, which captures and compares outputs to detect regressions.

Available snapshot types:

  • Image snapshots: Visual UI testing
  • JSON snapshots: Data structure testing
  • Inline snapshots: Text-based output testing

When to Use Snapshot Testing

Excellent for:

Complex Data Structures: Testing JSON responses, complex model transformations

// Example: Testing complex suggestion results
func testSuggestionResults() {
    let suggestions = suggestionLoader.getSuggestions(for: "duck")
    assertInlineSnapshot(of: suggestions.encoded(), as: .lines, matches: """
    Suggestions:
    - Search: "duck" (score: 100)
    - Bookmark: "DuckDuckGo" (https://duckduckgo.com)
    - History: "Duck typing" (https://en.wikipedia.org/wiki/Duck_typing)
    """)
}

Visual Regression Testing: UI components, layouts, visual elements

Algorithm Output Validation: Testing complex calculations, parsing results

Cross-Platform Consistency: Ensuring consistent output across iOS/macOS

Image Snapshot Testing

Perfect for visual regression testing:

import SnapshotTesting

func testButtonAppearance() {
    for appearanceName: NSAppearance.Name in [.aqua, .darkAqua] {
        NSApp.appearance = .init(named: appearanceName)!
        
        let button = createStyledButton()
        assertSnapshot(of: button, as: .image(perceptualPrecision: 0.9), named: appearanceName.rawValue)
    }
}

Image snapshot guidelines:

  • Test in both light and dark modes
  • Use perceptualPrecision: 0.9 for UI components (allows minor anti-aliasing differences)
  • Use perceptualPrecision: 1.0 for pixel-perfect requirements
  • Create consistent test environments (fixed window sizes, scale factors)

JSON Snapshot Testing

Ideal for data structure validation:

func testBookmarkImport() {
    let importResult = bookmarkImporter.importFromHTML(testFile)
    assertSnapshot(of: importResult.bookmarks, as: .json, named: "imported_bookmarks")
}

JSON snapshot benefits:

  • Catches structural changes in data models
  • Validates complex transformations
  • Provides clear diffs for debugging
  • Language-agnostic format

Inline Snapshot Testing

Best for text-based output and algorithm results:

func testSearchSuggestions() {
    let results = searchEngine.getSuggestions(for: "privacy")
    assertInlineSnapshot(of: results.encoded(), as: .lines, matches: """
    Privacy Settings
    Privacy Policy
    Privacy Tools
    """)
}

Inline snapshot advantages:

  • Snapshots live in the test file
  • Easy to review changes in pull requests
  • No external files to manage
  • Great for small, predictable outputs

Snapshot Testing Best Practices

Guidelines

  • Make tests deterministic: Use fixed dates, sorted collections, consistent ordering
  • Test meaningful changes: Don't snapshot trivial variations
  • Use descriptive names: named: "dark_mode_large_text" instead of named: "test1"
  • Review snapshot changes: Always review generated snapshots during development
  • Keep snapshots small: Large snapshots are hard to review and maintain

Memory Management with Snapshots

func testComplexView() {
    autoreleasepool {
        let window = SnapshotWindow()
        let view = ComplexView(data: testData)
        window.contentView = view
        
        assertSnapshot(of: view, as: .image())
    } // Ensures immediate cleanup
}

When NOT to Use Snapshot Testing

  • Highly dynamic content: Real-time data, user-specific content
  • Performance testing: Snapshot testing is about correctness, not speed
  • Simple value comparisons: Use XCTAssertEqual for basic assertions
  • Flaky systems: Outputs that vary between runs

Test Logging and Debugging

Using Logger.tests for Test Information

NEVER use print() in tests. ALWAYS use Logger.tests for debug output:

import os.log

 // GOOD: Use Logger.tests for test debugging
func testComplexFlow() {
    Logger.tests.info("Starting complex flow test")
    Logger.tests.debug("Setting up test data with \(testData.count) items")
    Logger.tests.debug("DEBUG: requestCount = \(requestCount), currentState = \(service.currentState)")
    
    // Perform test operations
    
    Logger.tests.log("Test completed successfully")
}

 // BAD: Using print() statements
func testComplexFlow() {
    print("Starting test")  // Never use print()
    print("DEBUG: requestCount = \(requestCount)")  // Use Logger.tests.debug() instead
}

Benefits of Logger.tests:

  • Structured logging that integrates with Xcode and CI systems
  • Proper log levels (info, debug, error)
  • Automatic collection in CI artifacts
  • Better performance than print() statements

Comprehensive Failure Logging

For CI debugging, provide comprehensive failure logs:

func testDataProcessing() {
    Logger.tests.info("Testing data processing with \(inputData.count) items")
    
    do {
        let result = try processor.process(inputData)
        Logger.tests.debug("Processing completed with \(result.count) results")
        
        XCTAssertEqual(result.count, expectedCount, 
                      "Expected \(expectedCount) results, got \(result.count). Input: \(inputData)")
    } catch {
        Logger.tests.error("Processing failed: \(error)")
        XCTFail("Processing failed with error: \(error)")
    }
}

Best practices for failure logging:

  • Include relevant context in assertion messages
  • Log the actual values that caused failures
  • Use structured logging for complex data
  • Include test setup information that might affect results

Best Practices

Test Naming

XCTest Syntax (traditional):

func testBookmarkCreation_WhenValidData_ShouldSucceed() { }
func testUserAuthentication_WithInvalidCredentials_ShouldFail() { }

@Test Syntax (Swift Testing - preferred for new tests):

@Test("Bookmark creation succeeds with valid data")
func bookmark_creation_with_valid_data() { }

@Test("User authentication fails with invalid credentials")
func user_authentication_fails_with_invalid_credentials() { }

@Test("External URLs are validated correctly")
func external_urls_are_valid() { }

@Test Naming Guidelines:

  • Use clear, descriptive text in @Test("description")
  • Function names should be simple and readable (no "test" prefix needed)
  • Use underscores for better readability in function names
  • Focus the description on what behavior is being verified

Test Organization

Group related tests using // MARK: comments:

// MARK: - Bookmark Creation Tests
// MARK: - Bookmark Deletion Tests

Async Testing

Basic Async Testing:

func testAsyncOperation() async {
    let result = await service.fetchData()
    XCTAssertNotNil(result)
}

Async Testing with Timeouts:

// Using withTimeout for async operations
func testAsyncOperationWithTimeout() async throws {
    let result = try await withTimeout(5.0) {
        return try await service.performLongOperation()
    }
    XCTAssertNotNil(result)
}

// Custom timeout error
func testAsyncOperationWithCustomError() async throws {
    do {
        _ = try await withTimeout(1.0, throwing: CustomTimeoutError()) {
            return try await service.performSlowOperation()
        }
        XCTFail("Should have timed out")
    } catch is CustomTimeoutError {
        // Expected timeout
    }
}

Publisher Testing with Timeouts:

// Testing publisher with timeout using extensions
func testPublisherWithTimeout() async throws {
    let subject = PassthroughSubject<String, Never>()
    
    // Use timeout extension for publishers
    let future = subject.timeout(2.0, "Publisher timeout").first().promise()
    
    DispatchQueue.main.async {
        subject.send("test value")
    }
    
    let result = try await future.get()
    XCTAssertEqual(result, "test value")
}

// For testing actual async behavior, use proper event-driven patterns:
func testPublisherWithAsyncBehavior() {
    let expectation = expectation(description: "Should receive published value")
    let subject = PassthroughSubject<String, Never>()
    
    let cancellable = subject
        .sink { value in
            XCTAssertEqual(value, "test value")
            expectation.fulfill()
        }
    
    // Trigger the real event that should cause publication
    service.performActionThatPublishes() // This internally calls subject.send()
    
    wait(for: [expectation], timeout: 1.0)
    cancellable.cancel()
}

Async Timeout Guidelines:

  • Use withTimeout() for Task-based async operations - prevents indefinite hanging
  • Use publisher .timeout() extensions for Combine workflows - handles stream timeouts gracefully
  • Choose appropriate timeout values: Short for unit tests (1-5s), longer for integration tests (10s+)
  • Always test timeout scenarios - ensure your code handles timeouts properly
  • Use descriptive timeout messages - helps with debugging when timeouts occur

🚨 CRITICAL: Async Testing Anti-Patterns

NEVER Use These Timing Patterns in Tests

NEVER use arbitrary delays in tests - they make tests flaky, slow, and unreliable:

 // BAD: Arbitrary time delays
func testBadPattern() {
    // NEVER DO THIS
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        expectation.fulfill()
    }
    
    // OR THIS
    Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
        expectation.fulfill()
    }
    
    // OR THIS
    Thread.sleep(forTimeInterval: 0.5)
    
    wait(for: [expectation], timeout: 5.0)
}

Use Event-Driven Testing Instead

Replace timing delays with event-driven expectations:

Pattern 1: Callback-Based Expectations

 // GOOD: Event-driven testing with callbacks
class MockService {
    var onEventTriggered: (() -> Void)?
    
    func triggerEvent() {
        // Do work...
        onEventTriggered?() // Signal completion
    }
}

func testGoodPattern() {
    let expectation = expectation(description: "Event should be triggered")
    
    mockService.onEventTriggered = {
        expectation.fulfill()
    }
    
    // Trigger the actual event
    mockService.triggerEvent()
    
    wait(for: [expectation], timeout: 1.0) // Short timeout for deterministic events
}

Pattern 2: Publisher-Based Expectations

 // GOOD: Combine publisher testing
func testPublisherPattern() {
    let expectation = expectation(description: "Publisher should emit value")
    
    let cancellable = service.statePublisher
        .compactMap { $0 }
        .first()
        .sink { value in
            XCTAssertEqual(value, .expectedState)
            expectation.fulfill()
        }
    
    // Trigger the state change
    service.updateState(.expectedState)
    
    wait(for: [expectation], timeout: 1.0)
    cancellable.cancel()
}

Pattern 3: Predicate-Based Expectations

 // GOOD: Condition-based waiting
func testPredicatePattern() {
    // Trigger the operation
    service.startOperation()
    
    // Wait for specific condition to be true
    let predicate = NSPredicate { _, _ in
        service.isOperationComplete
    }
    let expectation = XCTNSPredicateExpectation(predicate: predicate, object: nil)
    wait(for: [expectation], timeout: 2.0)
    
    XCTAssertTrue(service.isOperationComplete)
}

Pattern 4: Inverted Expectations for "Should Not Happen"

 // GOOD: Testing that something should NOT happen
func testShouldNotHappen() {
    let expectation = expectation(description: "Should not trigger unwanted behavior")
    expectation.isInverted = true // Test passes if expectation is NOT fulfilled
    
    mockService.onUnwantedEvent = {
        expectation.fulfill() // This would fail the test
    }
    
    // Trigger some action
    service.performAction()
    
    wait(for: [expectation], timeout: 1.0) // Short timeout for negative tests
    
    // Verify expected behavior occurred instead
    XCTAssertTrue(service.didPerformExpectedBehavior)
}

Why Event-Driven Testing Is Superior

  1. Deterministic: Tests wait for actual events, not arbitrary time
  2. Fast: No unnecessary delays - tests complete as soon as events occur
  3. Reliable: Eliminates race conditions and timing-dependent failures
  4. Maintainable: Clear relationship between triggers and expectations
  5. Debuggable: Failures point to actual logic issues, not timing problems

Migration Strategy

When you see these patterns in existing tests:

  1. Identify the real event the test is waiting for
  2. Add callback/publisher to the mock or service to signal that event
  3. Replace arbitrary delays with event-driven expectations
  4. Use shorter timeouts (1-2s) since events are deterministic

Remember: Good tests are event-driven, not time-driven!

Time/Date Testing Patterns

Critical Design Principle

ALWAYS inject time/date dependencies into your classes - NEVER use Date() or Task.sleep() directly in production code that needs testing.

🚫 FORBIDDEN: Task.sleep() in Tests

NEVER use Task.sleep() in any test code:

// ❌ NEVER DO THIS
try await Task.sleep(nanoseconds: 100_000_000) // Creates flaky tests
try await Task.sleep(for: .seconds(1))         // Unreliable timing
try? await Task.sleep(interval: 0.1)           // Arbitrary delays

// ✅ DO THIS INSTEAD
let expectation = expectation(description: "Wait for async operation")
someAsyncOperation {
    expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 1.0)

Why Task.sleep() is banned:

  1. Flaky tests - Real-world timing varies
  2. Slow tests - Fixed delays waste time
  3. Unreliable - May not wait long enough or wait too long
  4. Hides race conditions - Masks real timing issues

Timeout Guidelines

  • Keep test timeouts reasonable: maximum 5 seconds for most async operations
  • Use shorter timeouts (1-3 seconds) when possible to catch issues faster
  • Only use longer timeouts for truly slow operations (network requests, file I/O)

🚫 NEVER Change Tests to Match Wrong Behavior

When tests fail, fix the code, not the test:

// ❌ WRONG: Changing test to match broken behavior
XCTAssertEqual(result.count, 3, "Should return 3 items") // Changed from 5 to 3 to make test pass

// ✅ RIGHT: Fix the actual code issue
// Fix the logic to properly return all expected items
XCTAssertEqual(result.count, 5, "Should return all 5 items as originally designed")

Tests should verify correct behavior, not accommodate bugs. If a test fails:

  1. First - Check if the production code has a bug
  2. Fix the bug in the production code
  3. Only then update the test if the expected behavior has legitimately changed

Never adjust tests to hide problems in the implementation.

When a class needs current time or sleep functionality, inject these dependencies through the initializer:

 // GOOD: Injectable time dependencies
class MyService {
    private let dateProvider: () -> Date
    private let sleeper: Sleeper
    
    init(dateProvider: @escaping () -> Date = Date.init,
         sleeper: Sleeper = .default) {
        self.dateProvider = dateProvider
        self.sleeper = sleeper
    }
    
    func performOperation() async throws {
        let now = dateProvider() // ✅ Testable
        // ... do work ...
        try await sleeper.sleep(for: 1.0) // ✅ Testable
    }
}

 // BAD: Hard-coded time dependencies
class MyService {
    func performOperation() async throws {
        let now = Date() // ❌ Not testable
        // ... do work ...
        try await Task.sleep(nanoseconds: 1_000_000_000) // ❌ Not testable
    }
}

Pattern 1: TestClock for Async Sleep Testing

Use TestClock<Duration> for testing code that uses Task.sleep() or periodic operations:

 // GOOD: TestClock pattern for async sleep testing
func testPeriodicUpdates() async throws {
    let clock = TestClock<Duration>()
    let sleeper = Sleeper(clock: clock)
    
    // Inject the test sleeper
    let service = MyPeriodicService(sleeper: sleeper, interval: 2.0)
    
    let expectation1 = expectation(description: "First update")
    let expectation2 = expectation(description: "Second update")
    
    var updateCount = 0
    service.onUpdate = {
        updateCount += 1
        if updateCount == 1 {
            expectation1.fulfill()
        } else if updateCount == 2 {
            expectation2.fulfill()
        }
    }
    
    // Start the periodic task
    let task = service.startPeriodicUpdates()
    
    // Wait for first update (immediate)
    await fulfillment(of: [expectation1], timeout: 1.0)
    XCTAssertEqual(updateCount, 1)
    
    // Advance clock by 2 seconds to trigger next update
    await clock.advance(by: .seconds(2))
    await fulfillment(of: [expectation2], timeout: 1.0)
    XCTAssertEqual(updateCount, 2)
    
    task.cancel()
}

// Production service with injectable sleeper
class MyPeriodicService {
    private let sleeper: Sleeper
    private let interval: TimeInterval
    var onUpdate: (() -> Void)?
    
    init(sleeper: Sleeper = .default, interval: TimeInterval) {
        self.sleeper = sleeper
        self.interval = interval
    }
    
    func startPeriodicUpdates() -> Task<Void, Error> {
        Task.periodic(interval: interval, sleeper: sleeper) {
            await self.performUpdate()
        }
    }
    
    private func performUpdate() async {
        // Do work...
        onUpdate?()
    }
}

TestClock Best Practices

ALWAYS use Task.megaYield(count: N) after clock.advance() to allow async processing:

 // GOOD: Proper megaYield usage
await clock.advance(by: .seconds(5))
await Task.megaYield(count: 5)  // Allow async tasks to process

 // BAD: Multiple consecutive megaYield calls
await clock.advance(by: .seconds(5))
await Task.megaYield()
await Task.megaYield()
await Task.megaYield()

 // BAD: No megaYield after clock advance
await clock.advance(by: .seconds(5))
// Missing yield - async tasks may not complete

Why megaYield is critical:

  • TestClock advancement is synchronous, but triggered async tasks need time to process
  • megaYield(count: N) ensures async operations complete before assertions
  • Use count: 5 as a standard (allows multiple yield cycles)

Pattern 2: MockDateProvider for Date Testing

Use MockDateProvider or TimeTraveller for testing code that needs current date:

 // GOOD: MockDateProvider pattern
class MockDateProvider {
    private var date: Date
    
    init(date: Date = Date()) {
        self.date = date
    }
    
    func setNowDate(_ date: Date) {
        self.date = date
    }
    
    func advanceBy(_ timeInterval: TimeInterval) {
        date.addTimeInterval(timeInterval)
    }
    
    func getDate() -> Date {
        date
    }
}

func testDateBasedLogic() {
    let mockDateProvider = MockDateProvider(date: Date(timeIntervalSince1970: 0))
    let service = MyService(dateProvider: mockDateProvider.getDate)
    
    // Test with specific date
    let result1 = service.processData()
    XCTAssertEqual(result1.timestamp, Date(timeIntervalSince1970: 0))
    
    // Advance time and test again
    mockDateProvider.advanceBy(TimeInterval.days(1))
    let result2 = service.processData()
    XCTAssertEqual(result2.timestamp, Date(timeIntervalSince1970: 86400))
}

// Production service with injectable date provider
class MyService {
    private let dateProvider: () -> Date
    
    init(dateProvider: @escaping () -> Date = Date.init) {
        self.dateProvider = dateProvider
    }
    
    func processData() -> DataResult {
        return DataResult(timestamp: dateProvider(), data: "...")
    }
}

Pattern 3: Protocol-Based Date Injection

For more complex date/time scenarios, use protocol injection:

 // GOOD: Protocol-based date injection
protocol CurrentDateProviding {
    var currentDate: Date { get }
}

extension Date: CurrentDateProviding {
    public var currentDate: Date { self }
}

class MockDateProvider: CurrentDateProviding {
    var currentDate: Date
    
    init(currentDate: Date = Date()) {
        self.currentDate = currentDate
    }
}

class MyService {
    private let dateProvider: CurrentDateProviding
    
    init(dateProvider: CurrentDateProviding = Date()) {
        self.dateProvider = dateProvider
    }
    
    func isExpired(_ item: Item) -> Bool {
        return item.expiryDate < dateProvider.currentDate
    }
}

func testExpiryLogic() {
    let mockDateProvider = MockDateProvider(currentDate: Date(timeIntervalSince1970: 1000))
    let service = MyService(dateProvider: mockDateProvider)
    
    let expiredItem = Item(expiryDate: Date(timeIntervalSince1970: 500))
    let validItem = Item(expiryDate: Date(timeIntervalSince1970: 1500))
    
    XCTAssertTrue(service.isExpired(expiredItem))
    XCTAssertFalse(service.isExpired(validItem))
}

Pattern 4: Combined TestClock and MockDateProvider

For services that need both current time and sleep capabilities:

 // GOOD: Combined time and sleep mocking
func testServiceWithTimeAndSleep() async throws {
    let mockDateProvider = MockDateProvider(date: Date(timeIntervalSince1970: 0))
    let clock = TestClock<Duration>()
    let sleeper = Sleeper(clock: clock)
    
    let service = MyTimedService(
        dateProvider: mockDateProvider.getDate,
        sleeper: sleeper
    )
    
    let expectation = expectation(description: "Operation should complete")
    
    service.onOperationComplete = { result in
        // Verify the result includes the correct timestamp
        XCTAssertEqual(result.startTime, Date(timeIntervalSince1970: 0))
        expectation.fulfill()
    }
    
    // Start operation
    let task = service.startOperation()
    
    // Advance mock time (affects dateProvider)
    mockDateProvider.advanceBy(5.0)
    
    // Advance test clock (affects sleeper)
    await clock.advance(by: .seconds(1))
    
    await fulfillment(of: [expectation], timeout: 1.0)
    task.cancel()
}

Time Testing Best Practices

  1. Always inject time dependencies - Never use Date() or Task.sleep() directly in production code
  2. Use TestClock for async operations - When testing Task.sleep(), Task.periodic, or Sleeper
  3. Use MockDateProvider for date logic - When testing date comparisons, timestamps, or date-based decisions
  4. Test time progression - Use advance() methods to test how your code behaves over time
  5. Test boundary conditions - Test behavior at midnight, month boundaries, leap years, etc.
  6. Keep time control granular - Advance time by specific amounts rather than arbitrary delays

Common Time Testing Mistakes

Don't use real time in tests:

// BAD: Unreliable and slow
func testBadTimePattern() async {
    service.scheduleTask()
    try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
    XCTAssertTrue(service.taskCompleted)
}

Use mock time instead:

// GOOD: Fast and deterministic
func testGoodTimePattern() async {
    let clock = TestClock<Duration>()
    let service = MyService(sleeper: Sleeper(clock: clock))
    
    let expectation = expectation(description: "Task should complete")
    service.onTaskComplete = { expectation.fulfill() }
    
    service.scheduleTask()
    await clock.advance(by: .seconds(2))
    
    await fulfillment(of: [expectation], timeout: 1.0)
}

Remember: Control time in tests, don't wait for it!

Error Testing

Test both success and failure cases:

func testNetworkError_ShouldHandleGracefully() {
    // Test error handling
}

Skipping Tests

Use XCTSkip to temporarily disable problematic tests:

// XCTest syntax
func testFlakySomething() throws {
    throw XCTSkip("Flaky test - temporarily disabled")
}

// @Test syntax
@Test("Temporarily disabled flaky test")
func flaky_behavior() throws {
    throw XCTSkip("Flaky test - investigating timing issues")
}

XCTSkip Guidelines:

  • Use throw XCTSkip("reason") to temporarily disable flaky or problematic tests
  • Function must be marked as throws to use XCTSkip
  • Include a clear reason in the skip message for future investigation

CI/CD Integration

Tests are automatically run in GitHub Actions with:

  • Parallel execution for iOS and macOS
  • Automatic log collection on failures
  • JUnit report generation
  • Crash report collection
  • Asana integration for failure tracking

Debugging Failed Tests

1. Check Logs

Download log artifacts from GitHub Actions

2. Reproduce Locally

Use command line tools for consistent reproduction:

Run macOS tests

xcodebuild test
-scheme "macOS Browser"
-configuration "Debug"
-skipPackagePluginValidation
-skipMacroValidation
-allowProvisioningUpdates=NO
-only-testing:Unit\ Tests


```bash
# Run specific macOS Unit Test (e.g., HotspotDetectionServiceTests)
xcodebuild test \
  -scheme "macOS Browser" \
  -configuration "Debug" \
  -skipPackagePluginValidation \
  -skipMacroValidation \
  -only-testing:Unit\ Tests/HotspotDetectionServiceTests \
  -allowProvisioningUpdates=NO

Integration Tests

# Run specific macOS Integration Test (e.g., DownloadsIntegrationTests)
xcodebuild test \
  -scheme "macOS Browser" \
  -configuration "Debug" \
  -skipPackagePluginValidation \
  -skipMacroValidation \
  -only-testing:Integration\ Tests/DownloadsIntegrationTests \
  -allowProvisioningUpdates=NO

# Run all Integration Tests
xcodebuild test \
  -scheme "macOS Browser" \
  -configuration "Debug" \
  -skipPackagePluginValidation \
  -skipMacroValidation \
  -only-testing:Integration\ Tests \
  -allowProvisioningUpdates=NO

UI Tests

# Run iOS UI tests
xcodebuild test \
  -scheme "iOS Browser" \
  -workspace DuckDuckGo.xcworkspace \
  -destination "platform=iOS Simulator,name=iPhone 15 Pro" \
  -only-testing:UITests

# Run macOS UI tests
xcodebuild test \
  -scheme "macOS UI Tests" \
  -configuration "Review" \
  -skipPackagePluginValidation \
  -skipMacroValidation \
  -allowProvisioningUpdates=NO \
  -only-testing:UI\ Tests

Run specific macOS UI test case

xcodebuild test
-scheme "macOS UI Tests"
-configuration "Review"
-skipPackagePluginValidation
-skipMacroValidation
-allowProvisioningUpdates=NO
-only-testing:UI\ Tests/DownloadsUITests


> ⚠️ **AI Assistant Note**: Never run test commands automatically without explicit user permission.

### 3. Reproduce Flaky Tests
Reduce timeouts to increase failure rate locally:

```swift
// Temporarily reduce timeouts to catch race conditions
let shortTimeout = 0.1 // Instead of 5.0
XCTAssertTrue(element.waitForExistence(timeout: shortTimeout))

4. Run Multiple Times

Use Xcode's repeat testing options:

  • Test Navigator: Right-click test → "Run [TestName] Repeatedly..."
  • Test Settings: Set "Run" to repeat 10-100 times
  • Without Building: Hold Option when clicking Run button for "Run Without Building"
  • Command Line: Use --repeat-count with xcodebuild

5. Memory Debugging

Use Memory Browser and allocation tracking:

# Enable malloc stack logging for detailed memory traces
export MallocStackLogging=1

# Run tests with memory debugging
xcodebuild test -scheme YourScheme

In Xcode:

  • Memory Browser: Debug → Debug Workflow → View Memory
  • Malloc Stack Logging: Enable in scheme environment variables
  • AutoreleaseTracker: Search for "AutoreleaseTracker" in Memory Browser to find leaked objects
  • Stack Traces: View allocation stack traces for retained objects

For questions or improvements to this guide, please contribute to the documentation or reach out to the iOS/macOS team.

Recovering UI Automation Mode (MANDATORY when runner fails to initialize)

When every UI test fails immediately with errors like:

  • "The test runner failed to initialize for UI testing. (Underlying Error: Timed out while enabling automation mode.)"

Follow these steps in order:

  1. Verify and re-grant Privacy permissions
  • System Settings → Privacy & Security → Accessibility: enable Terminal and Xcode/Xcode-beta
  • System Settings → Privacy & Security → Automation: under Xcode/Xcode-beta, allow controlling “System Events” and Finder
  1. Refresh the user session
  • Quit Xcode and Terminal
  • Log out and back in (preferred) or reboot the machine
  • After login, open Xcode once to re-establish automation trust prompts
  1. Optional: Reset TCC entries (you will need to re-grant prompts)
tccutil reset Accessibility com.apple.dt.Xcode com.apple.dt.Xcode-Beta com.apple.Terminal
tccutil reset AppleEvents com.apple.dt.Xcode com.apple.dt.Xcode-Beta com.apple.Terminal
  1. Sanity-check with a minimal known-green test
  • Run a single previously passing UI test/class (e.g., HTTPSUpgradeUITests) before running failing classes

Notes:

  • Restarting testmanagerd is restricted by SIP on recent macOS versions and usually not necessary once you refresh the session.
  • Record environment-related failures in failing-ui-tests.md and resume class-by-class once automation is restored.

Parsing xcresult Failures (MANDATORY)

Always extract failures from .xcresult to drive fixes. Use these exact steps:

  1. Dump legacy JSON to a temp file
xcrun xcresulttool get object --format json --legacy --path \
"/Users/admin/Library/Developer/Xcode/DerivedData/DuckDuckGo-<HASH>/Logs/Test/Test-macOS UI Tests-YYYY.MM.DD_HH-MM-SS-+ZZZZ.xcresult" \
> /tmp/xc_root.json
  1. Pull top-level failure summaries from ActionResult.issues.testFailureSummaries
python3 - << 'PY'
import json, re
j=json.load(open('/tmp/xc_root.json'))
vals = j.get('actions',{}).get('_values') or []
if not vals:
    raise SystemExit('no actions in xcresult json')
act = vals[0]
fails = ((act.get('actionResult',{})
           .get('issues',{})
           .get('testFailureSummaries',{})
           .get('_values')) or [])

def decode_url(url):
    if not url: return ('','')
    m = re.match(r'^file:\/\/(.*?)#.*StartingLineNumber=(\d+)', url)
    return (m.group(1), m.group(2)) if m else (url,'')

for f in fails:
    name = (f.get('testCaseName') or {}).get('_value','')
    msg  = (f.get('message') or {}).get('_value','')
    url  = (f.get('documentLocationInCreatingWorkspace') or {}).get('url',{}).get('_value','')
    filePath, line = decode_url(url)
    print(f"{name}\t{' '.join(msg.split())}\t{filePath}\t{line}")
PY

Notes:

  • Use --legacy; the non-legacy command is deprecated and returns no JSON here.
  • If per-test details aren't under the top-level summaries, traverse actions._values[].actionResult.testsRef and follow summaryRef ids for each leaf test to collect failureSummaries.
  • Never guess failures by console output; always parse .xcresult.
  1. Update failing-ui-tests.md with a flat list of tests and exact reasons (include file:line when available). Then fix tests one by one, marking progress as:
  • fixed/unchecked
  • validated
  • still failing — reason