UI Testing Implementation

Overview

This document tracks the implementation of UI automation testing with screenshots.

Key Requirements:

  • Tests run with isolated test storage (not user’s real data)
  • Screenshots captured at each step for visual regression
  • Fixtures look like real person’s data (not “test1”, “test2”)
  • Each step follows the Backlog Processing Workflow (see 910-backlog)

Backlog Processing Workflow

For each implementation step:

  1. Plan Agent (thorough) - Deep analysis, identify files, design approach
  2. Review Agent (thorough) - Verify plan, check edge cases, suggest improvements
  3. Implement Agent - Write code following approved plan
  4. Test/Document Agent - Verify it works, update docs

Implementation Steps

Step 1: App-Side Test Storage Support

  • Modify StorageLocationManager to accept --test-storage= launch argument
  • Ensure override happens before AppState initialization
  • Status: Completed
  • Files modified: TimeTracker/Sources/Services/StorageLocationManager.swift

Step 2: Add UI Test Target

  • Update project.yml with TimeTrackerUITests target
  • Create directory structure (UITests/, UITests/Flows/, UITests/Fixtures/)
  • Note: swift-snapshot-testing not used (Xcode 26 compatibility issue), using XCTest native screenshots
  • Status: Completed
  • Files modified: TimeTracker/project.yml

Step 3: Create Test Fixtures

  • Create realistic fixture data (real person’s usage pattern)
  • empty/ - Clean slate with empty tags.json
  • standard/ - Tags and records for a freelance developer
  • Status: Completed
  • Files created: UITests/Fixtures/empty/, UITests/Fixtures/standard/

Step 4: Add Accessibility Identifiers

  • Add identifiers to key interactive elements
  • Status: Completed
  • Identifiers added:
    • floatingPlayButton - Main play button
    • settingsButton - Settings gear
    • stopTimerButton - Stop button in running timer (Note: inherits section identifier due to SwiftUI behavior)
    • runningTimersSection - Running timers section
    • useDefaultLocationButton - Storage setup button

Step 5: Base Test Class

  • Create TimeTrackerUITests.swift with fixture copying
  • Add snapshot helper methods using XCTest native screenshots
  • Add element waiting utilities
  • Status: Completed
  • Files created: UITests/TimeTrackerUITests.swift

Step 6: First Test - Timer Flow

  • Implement TimerFlowTests.swift
  • Verify screenshots work
  • Generate baseline snapshots
  • Status: Completed
  • Files created: UITests/Flows/TimerFlowTests.swift
  • Snapshots saved to: UITests/__Snapshots__/TimerFlowTests/

Step 7: CI Integration

  • GitHub Actions workflow for UI tests
  • Artifact upload for failed snapshots
  • Status: Not started

Running UI Tests

# Run all UI tests
cd TimeTracker && xcodebuild test -scheme TimeTracker -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:TimeTrackerUITests

# Run specific test
xcodebuild test -scheme TimeTracker -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:TimeTrackerUITests/TimerFlowTests/testStartAndStopTimer

Test Architecture

Test Storage Isolation

  • Tests pass --test-storage=/path/to/temp as launch argument
  • StorageLocationManager detects this and uses the path instead of user’s Documents
  • Each test gets a unique temp directory that’s cleaned up after

Fixture System

  • Fixtures stored in UITests/Fixtures/
  • Each fixture is a directory with tags.json and optional records/ subdirectory
  • Tests override fixtureName property to select fixture
  • Base class copies fixture to temp directory before app launch

Screenshots

  • Saved to UITests/__Snapshots__/{TestClassName}/{name}.png
  • Also attached to Xcode test results for viewing
  • Use snapshot("name") helper method

Fixture Data Design

Fixtures should look like a real person’s time tracking data.

Character: Alex (Freelance Developer)

  • Works on client projects: “Acme Corp”, “StartupXYZ”
  • Has personal projects: “Side Project”, “Learning”
  • Uses comments like real notes, not test strings

Tags (standard fixture)

NameColorNotes
Acme Corpoklch(0.5579 0.1147 240)Main client (blue)
StartupXYZoklch(0.5579 0.1147 145)Secondary client (green)
Side Projectoklch(0.5579 0.1147 300)Personal coding (purple)
Learningoklch(0.5579 0.1147 45)Courses, reading (orange)
Adminoklch(0.5579 0.1147 0)Emails, planning (gray)

Sample Records Pattern

  • Morning: Acme Corp work session (2-3 hours)
  • Midday: StartupXYZ meeting or work
  • Afternoon: Mixed tasks
  • Evening: Side project or learning
  • Comments reference real-sounding tasks

Known Issues and Solutions

SwiftUI Accessibility Identifier Inheritance

When a Section has an accessibilityIdentifier, child elements may inherit it. Solution: Find elements by label instead of identifier when needed.

// Instead of:
let stopButton = app.buttons["stopTimerButton"]

// Use:
let stopButton = app.buttons.matching(NSPredicate(format: "label == 'Stop'")).firstMatch

Xcode 26 and swift-snapshot-testing

The swift-snapshot-testing package has compatibility issues with Xcode 26 beta. Solution: Using XCTest’s native XCUIScreen.main.screenshot() instead.


Progress Log

2025-12-18

  • Created implementation document
  • Implemented Steps 1-6 (all core functionality)
  • First UI test (TimerFlowTests) passing
  • Screenshots saving correctly to UITests/__Snapshots__/
  • Discovered SwiftUI accessibility identifier inheritance issue and documented workaround

Related