Files

1669 lines
53 KiB
Markdown

---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/testing.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: 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:
```swift
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:
```swift
func testURLSchemeDetection() {
let detector = URLSchemeDetector()
XCTAssertTrue(detector.isValidURL("https://duckduckgo.com"))
XCTAssertFalse(detector.isValidURL("invalid-url"))
}
```
#### Utility Functions
Testing helper functions, extensions, formatters:
```swift
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:
```swift
func testViewModelStateTransition() {
let viewModel = SearchViewModel()
viewModel.performSearch("test query")
XCTAssertEqual(viewModel.state, .loading)
}
```
### ❌ What to Avoid
#### Simple Property Toggles
Testing trivial getters/setters:
```swift
// ❌ 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:
```swift
// ❌ 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
```swift
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
```swift
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.
```swift
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
```swift
// ❌ 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
```swift
// ❌ 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
```swift
// ❌ 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
```swift
override var allowedNonNilVariables: Set<String> {
["constantData", "sharedManager", "staticConfiguration"]
}
```
### When to Use autoreleasepool {}
**Always Required**:
```swift
// 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
```swift
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:
```swift
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
```swift
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**:
```bash
# 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`):
```bash
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
```swift
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
```swift
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](ui-testing.md)**.
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
```swift
// 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:
```swift
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:
```swift
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:
```swift
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
```swift
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:**
```swift
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:
```swift
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):
```swift
func testBookmarkCreation_WhenValidData_ShouldSucceed() { }
func testUserAuthentication_WithInvalidCredentials_ShouldFail() { }
```
#### @Test Syntax (Swift Testing - preferred for new tests):
```swift
@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:
```swift
// MARK: - Bookmark Creation Tests
// MARK: - Bookmark Deletion Tests
```
### Async Testing
#### Basic Async Testing:
```swift
func testAsyncOperation() async {
let result = await service.fetchData()
XCTAssertNotNil(result)
}
```
#### Async Testing with Timeouts:
```swift
// 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:
```swift
// 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:**
```swift
❌ // 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
```swift
✅ // 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
```swift
✅ // 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
```swift
✅ // 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"
```swift
✅ // 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:**
```swift
// ❌ 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:**
```swift
// ❌ 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:
```swift
✅ // 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:**
```swift
✅ // 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:**
```swift
✅ // 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:**
```swift
✅ // 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:**
```swift
✅ // 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:**
```swift
✅ // 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:**
```swift
// 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:**
```swift
// 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:
```swift
func testNetworkError_ShouldHandleGracefully() {
// Test error handling
}
```
### Skipping Tests
Use `XCTSkip` to temporarily disable problematic tests:
```swift
// 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
```bash
# 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
```bash
# 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:
```bash
# 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
2) 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
3) Optional: Reset TCC entries (you will need to re-grant prompts)
```bash
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
```
4) 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
```bash
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
```
2) Pull top-level failure summaries from ActionResult.issues.testFailureSummaries
```bash
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`.
3) 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