Is it possible to assert Snapshot in between TestStore .send and .receive asserts?

Hi,

Is there a way to do SnapshotTesting by using the .do function when asserting on the TestStore in between .send and .receive ?

    testStore.assert([
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "initialState") },
      .send(.mapContainer(.mapView(.locationManager(.didUpdateLocations([parisLocation]))))) {
        $0.mapContainer.mapView.userLocationCoordinate = parisLocation
      },
      .receive(.mapContainer(.mapView(.centerOnUserReceived))) {
        $0.mapContainer.mapView.mapCenter = parisLocation.coordinate
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterDidUpdateLocations(\(parisLocation)") },
      .send(.mapContainer(.floatingButtonView(.searchAgainTapped))),
      .receive(.refreshItemsFromMapCoordsReceived),
      .receive(.overlayContainer(.overlayLocal(.updateLevelReceived(.minimum)))),
      .receive(.mapContainer(.floatingButtonView(.hideSearchAgainReceived(true)))),
      .receive(.overlayContainer(.overlayLocal(.refreshPositionReceived))) {
        $0.overlayContainer.overlayLocal.position.height = -60
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterSearchAgainTapped") },
      .send(.mapContainer(.mapView(.userTappedPOI(selectedPro)))) {
        $0.selectedPro = selectedPro
        $0.content = .fd
      },
      .receive(.overlayContainer(.overlayLocal(.updateLevelReceived(.half)))) {
        $0.overlayContainer.overlayLocal.level = .half
      },
      .receive(.mapContainer(.mapView(.centerOnSelectedProReceived))) {
        $0.mapContainer.mapView.mapCenter = selectedPro?.coordinates
      },
      .receive(.overlayContainer(.overlayLocal(.refreshPositionReceived))) {
        $0.overlayContainer.overlayLocal.position.height = 0
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterUserTappedPOI\(selectedPro)") },
    ])

For this to work we need TestStore to provide a way to return a proper Store and feed TestStore to the View.

1 Like

I would love to see something like this baked right in. But for now, how about an extension like this?

import SwiftUI
import ComposableArchitecture
import SnapshotTesting

extension TestStore.Step {
    static func receive<V: View>(
        _ action: Action,
        view: @escaping (Store<LocalState, LocalAction>) -> V,
        _ update: @escaping(inout LocalState) -> Void
    ) -> Self {
        .receive(action, { state in
            let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
            let view = view(store)
            assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
            update(&state)
            
        })
    }
    
    static func send<V: View>(
        _ action: LocalAction,
        view: @escaping (Store<LocalState, LocalAction>) -> V,
        _ update: @escaping(inout LocalState) -> Void
    ) -> Self {
        .send(action, { state in
            let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
            let view = view(store)
            assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
            update(&state)
            
        })
    }
}

I would love to see if we can improve ergonomics on this function even more. An obvious start would be to parameterize the Snapshotting strategy.

3 Likes

Hey Luke, this solution works great. I think I've made a few improvements:

  • Passing in parameters for #line, #function, and #file improves the test output in Xcode and helps organize the snapshots
  • Calling update(&state) first ensures that the state change is applied before the snapshot is taken.
  • Calling assertSnapshot in a DispatchQueue.main.async block seems to speed up the tests by an order of magnitude (100-300ms down to 10-20ms). I think this is because instead of blocking at each step, the snapshots are deferred until the next iteration of the run loop and executed as a batch.

Hi, just want to bump this topic. Do we have any update on this?

Hi @remlostime! Is there anything you're specifically asking for an update of? There seem to be some suggested solutions to the problem earlier in the thread, though they may need to be updated for the TestStore's latest API.

@stephencelis Thanks for the reply. I double checked the comments, and find the solution is a little bit out of date. I refactored it to fit the latest TCA and fix some bugs.

extension TestStore where Action: Equatable, LocalState: Equatable {
  func receive<V: View>(
    _ action: Action,
    view: @escaping (Store<LocalState, LocalAction>) -> V,
    update: @escaping(inout LocalState) -> Void
  ) {
    receive(action, { state in
      update(&state)
      let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
      let view = view(store)
      assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
    })
  }

  func send<V: View>(
    _ action: LocalAction,
    view: @escaping (Store<LocalState, LocalAction>) -> V,
    update: @escaping(inout LocalState) -> Void
  ) {
    send(action, { state in
      update(&state)
      let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
      let view = view(store)
      assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
    })
  }
}