Hi mates. I'm looking for advice on how to handle ...
# compose
c
Hi mates. I'm looking for advice on how to handle
TextFieldState
and
snapshotFlow
inside a
ViewModel
unit test as well as an assessment of my implementation. My
ViewModel
is using
snapshotFlow
on
TextFieldState.text
to derive error state for the text field it is associated with. However in unit tests
snapshotFlow
is only emitting the initial value but does not emit on changes of
TextFieldState.text
.
Copy code
data class UiState(
  val textState: TextFieldState,
  // Will be displayed as supportingText
  val textError: String?,
)

class MyViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(...)
  val uiState = _uiState.asStateFlow()

  init {
    viewModelScope.launch {
      val state = _uiState.value
      snapshotFlow { state.textState.text }
          .debounce(500L) // Debounce to give the user time to finish his writing
          .collectLatest {
            if (!isValid(it)) {
              _uiState.update {
                it.copy(textError = "Error")
              }
            }
          }
    }
  }
}

class MyViewModelTest {
  @Test
  fun errorTest() = runTest {
    val viewModel = MyViewModel()
    var state = viewModel.uiState.value
    
    state.textState.setTextAndPlaceCursorAtEnd("Invalid text")
    advanceUntilIdle()

    state = viewModel.uiState.value
    assertThat(state.textError).isEqual("Error")
  }
}
🧵 1
During research I found out that Snapshot needs something to notify about changes to state which is usually handled by the Compose runtime. From [1] I got that
Snapshot.sendApplyNotifications
after changes to state helps but it seems tedious and error prone to call on every change of state in unit test. I copied the implementation of from
GlobalSnapshotManager
[2] into a JUnit 5 Extension that seems to work to notify the Snapshot system on state changes. Unfortunately I have problems understanding how
Snapshot.registerGlobalWriteObserver
mechanism interoperates with state changes from test, to be sure that I have the right implementation.
Copy code
@OptIn(ExperimentalCoroutinesApi::class)
class SnapshotSchedulerExtension(
    private val dispatcher: TestDispatcher,
) : BeforeEachCallback, AfterEachCallback {

    private val started = AtomicBoolean(false)
    private val sent = AtomicBoolean(false)

    private var observerHandle: ObserverHandle? = null

    override fun beforeEach(context: ExtensionContext?) {
        if (started.compareAndSet(false, true)) {
            val channel = Channel<Unit>(1)
            CoroutineScope(dispatcher).launch {
                channel.consumeEach {
                    sent.set(false)
                    Snapshot.sendApplyNotifications()
                }
            }

            observerHandle = Snapshot.registerGlobalWriteObserver {
                if (sent.compareAndSet(false, true)) {
                    channel.trySend(Unit)
                }
            }
        }
    }

    override fun afterEach(context: ExtensionContext?) {
        observerHandle?.dispose()
        observerHandle = null

        started.set(false)
        sent.set(false)
    }
}
[1] https://kotlinlang.slack.com/archives/CJLTWPH7S/p1671685168586409 [2] https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]androidx/compose/ui/platform/GlobalSnapshotManager.android.kt