pointfreeco / swift-snapshot-testing

📸 Delightful Swift snapshot testing.
https://www.pointfree.co/episodes/ep41-a-tour-of-snapshot-testing
MIT License
3.78k stars 580 forks source link

Testing AsyncImage from SwiftUI #701

Open Filipsky5 opened 1 year ago

Filipsky5 commented 1 year ago

Describe the bug I would like to write a snapshot test that will test my SwiftUI view containing AsyncImage. To not be dependent on external API which holds images I added png files to my codebase and created a URL to it with Bundle(for: type(of: self)).url(forResource: "testImage", withExtension: "png"). To problem is that even with .wait strategy for assertSnapshots I'm not able to record snapshots with images, I'm only getting a placeholder. To Repro TestingAsyncImage.zip duce

// And/or enter code that reproduces the behavior here.

        let bundle = Bundle(for: type(of: self))
        let iconUrl = bundle.url(forResource: "testImage", withExtension: "png")!
        let view = ContentView(viewModel: MainViewModel(model: Model(id: Int.random(in: 0...3456), iconURL:iconUrl)))
        assertSnapshots(matching: view, as: [.wait(for: 2, on: .image)])
struct Model: Identifiable {
    let id: Int
    let iconURL: URL
}

final class MainViewModel: ObservableObject {
    var model: Model
    init(model: Model) {
        self.model = model
    }
}
struct ContentView: View {
    @ObservedObject var viewModel:
    MainViewModel
    var body: some View {
        VStack {
            AsyncImage(url: viewModel.model.iconURL) { phase in
                if let image = phase.image {
                    image.resizable().aspectRatio(contentMode: .fill)
                } else if phase.error != nil {
                    Color.red
                } else {
                    Color.blue
                }
            }.padding()
        }
    }
}

Expected behavior I would expect that adding .wait strategy for 2 seconds to assertSnapshots is long enough for AsyncImage to load the image from the URL which leads to local memory.

Screenshots

testExample 1

Environment

moto0000 commented 1 year ago

Hi @Filipsky5, I had the same problem. I solved it by overwriting AsyncImage: AsyncImage.swift

Now you can overwrite the image for the given url like this:

struct AsyncImagePreview: PreviewProvider {
  static var previews: some View {
    AsyncImage(url: URL(string: "http://example.com/image.png"))
      .environment(\.imageProvider) { url in
        guard url.absoluteString == "http://example.com/image.png"
        else { return nil }
        return UIImage(named: "mock")
      }
  }
}
dfeinzimer commented 10 months ago

I think I wrote a simple reproducer that highlights this problem after encountering it myself. I'm having the same issue testing views that have a task modifier.

It seems that wait(for:on:) isn't actually waiting as I'd expect and instead the SwiftUI view is being snapshotted as it appeared in its initial state.

import SnapshotTesting
import SwiftUI
import XCTest

struct TaskView: View {
    @State private var taskStarted = false

    @State private var taskFinished = false

    var body: some View {
        VStack {
            Text("Task Started: \(taskStarted.description)")
            Text("Task Finished: \(taskFinished.description)")
        }
        .task {
            taskStarted = true
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            taskFinished = true
        }
    }
}

final class SwiftUISnapshotTests: XCTestCase {
    func testTaskView() throws {
        let taskView = TaskView()
            .fixedSize()
        assertSnapshot(of: taskView, as: .wait(for: 6.0, on: .image))
    }
}

Snapshot: testTaskView 1

After the 6 second wait, the 5 second artificial task has had plenty of time to complete and so you'd expect both Task Started and Task Finished to say true yet they indicate the initial state of the view.


Update: After reading the documentation of wait(for:on:) I believe this isn't a bug but rather a misunderstanding of the purpose of wait(for:on:) paired with a current limitation of swift-snapshot-testing.

I've found a couple of other discussion that I think are all focused around this same limitation, namely #582 and #669.