AgentSkillsCN

snapshot

使用 SnapshotTesting 为 SwiftUI View 创建快照测试。适用于为支持 DSAsyncImage 的视图创建视觉回归测试时。

SKILL.md
--- frontmatter
name: snapshot
description: Creates Snapshot Tests for SwiftUI Views using SnapshotTesting. Use when creating visual regression tests for views with DSAsyncImage support.

Skill: Snapshot Tests

Guide for creating Snapshot Tests using Point-Free's SnapshotTesting library.

When to use this skill

  • Create snapshot tests for a View
  • Test different view states visually
  • Ensure visual regression prevention

Additional resources

  • For complete implementation examples, see examples.md

Prerequisites

  1. SnapshotTesting dependency in snapshot test targets
  2. DSAsyncImage component (replaces AsyncImage)
  3. ImageLoaderMock in CoreMocks
  4. Test image in Tests/Shared/Resources/

File structure

code
Tests/
├── Snapshots/                        # Snapshot tests
│   └── Presentation/
│       └── {Name}/
│           ├── {Name}ViewSnapshotTests.swift
│           └── __Snapshots__/
└── Shared/                           # Shared resources
    ├── Stubs/
    │   └── {Name}ViewModelStub.swift
    └── Resources/
        └── test-avatar.jpg

Key Components

DSAsyncImage

Views must use DSAsyncImage instead of AsyncImage. Uses AsyncImagePhase for handling states:

swift
DSAsyncImage(url: character.imageURL) { phase in
    switch phase {
    case .success(let image):
        image.resizable().scaledToFill()
    case .empty:
        ProgressView()
    case .failure:
        Image(systemName: "photo")
    @unknown default:
        ProgressView()
    }
}

ViewModel Protocol

Create a protocol for stub injection:

swift
protocol {Name}ViewModelContract: AnyObject {
    var state: {Name}ViewState { get }
    func load() async
}

ViewModel Stub

Returns fixed state without logic:

swift
@Observable
final class {Name}ViewModelStub: {Name}ViewModelContract {
    var state: {Name}ViewState

    init(state: {Name}ViewState) {
        self.state = state
    }

    func load() async { }
}

SnapshotStubs

swift
enum SnapshotStubs {
    static var testImage: UIImage? {
        guard let path = Bundle.module.path(forResource: "test-avatar", ofType: "jpg") else {
            return nil
        }
        return UIImage(contentsOfFile: path)
    }
}

Test Structure

Uses instance variables pattern for cleaner tests:

swift
struct {Name}ViewSnapshotTests {
    // MARK: - Properties

    private let imageLoader: ImageLoaderMock

    // MARK: - Initialization

    init() {
        UIView.setAnimationsEnabled(false)
        imageLoader = ImageLoaderMock(image: SnapshotStubs.testImage)
    }

    // MARK: - Tests

    @Test("Renders loading state correctly")
    func loadingState() {
        // Given
        let viewModel = {Name}ViewModelStub(state: .loading)

        // When
        let view = NavigationStack {
            {Name}View(viewModel: viewModel)
        }
        .imageLoader(imageLoader)

        // Then
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }

    @Test("Renders loaded state correctly")
    func loadedState() {
        // Given
        let viewModel = {Name}ViewModelStub(state: .loaded({Name}.stub()))

        // When
        let view = NavigationStack {
            {Name}View(viewModel: viewModel)
        }
        .imageLoader(imageLoader)

        // Then
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }
}

Benefits of instance variables:

  • imageLoader created once in init()
  • Each test only sets up test-specific state (viewModel)
  • Cleaner // Given section

Key Rules

Test Setup

  1. Disable animations: UIView.setAnimationsEnabled(false)
  2. Create ImageLoaderMock with test image
  3. Inject .imageLoader(imageLoader) on view

View Configuration

  1. Wrap in NavigationStack
  2. Use .iPhone13ProMax device config
  3. Apply imageLoader modifier

Naming

  • Test file: {Name}ViewSnapshotTests.swift
  • Test method: {stateName}State() (e.g., loadingState)
  • Test description: @Test("Renders {state} state correctly")
  • Snapshots folder: __Snapshots__/{Name}ViewSnapshotTests/

Running Tests

First Run (Recording)

bash
tuist test {Module}

First run creates references and fails (expected).

Subsequent Runs

bash
tuist test {Module}

Regenerate Snapshots

bash
rm -rf Tests/Snapshots/Presentation/{Name}/__Snapshots__
tuist test {Module}  # Run twice

Checklist

Setup (once per feature)

  • Add SnapshotTesting to snapshotTestDependencies
  • Create test image in Tests/Shared/Resources/
  • Create ViewModel stub in Tests/Shared/Stubs/
  • Create ViewModel protocol

Per View

  • Use DSAsyncImage in View
  • Create snapshot tests file in Tests/Snapshots/
  • All @Test attributes include a description
  • Initialize ImageLoaderMock
  • Test each state
  • Use .iPhone13ProMax config
  • Run tests twice (record + verify)