Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/molecule/molecule-public/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ kotlin {
implementation(libs.jetbrains.compose.runtime)
implementation(libs.coroutines.core)
}
jvmTest.dependencies {
implementation(libs.assertK)
implementation(libs.coroutines.test)
implementation(libs.junit)
implementation(libs.turbine)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
package com.hedvig.android.molecule.public

import androidx.compose.runtime.MonotonicFrameClock
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay

internal actual val mainDispatcher: CoroutineContext = Dispatchers.Main
/**
* [Dispatchers.Main] on the JVM carries no [MonotonicFrameClock], which `RecompositionMode.ContextClock` requires, so
* one is added here.
*/
internal actual val mainDispatcher: CoroutineContext = Dispatchers.Main + FixedRateFrameClock

/**
* Hands out a frame every 16 ms, close to the 60 Hz cadence the Android and iOS display clocks give. The wait is a
* [delay], so it runs on the frame's dispatcher (the Swing timer in a desktop app, virtual time under a test
* dispatcher) and needs no UI window to exist.
*/
private object FixedRateFrameClock : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
delay(FRAME_DURATION_MILLIS)
return onFrame(System.nanoTime())
}

private const val FRAME_DURATION_MILLIS = 16L
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.hedvig.android.molecule.public

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import app.cash.turbine.test
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

class MoleculeViewModelTest {
@Before
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
}

@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun `the presenter emits a new state after an event`() = runTest {
val viewModel = CounterViewModel()

viewModel.uiState.test {
assertThat(awaitItem()).isEqualTo(0)
viewModel.emit(CounterEvent.Increment)
assertThat(awaitItem()).isEqualTo(1)
}
}
}

private sealed interface CounterEvent {
data object Increment : CounterEvent
}

private class CounterViewModel : MoleculeViewModel<CounterEvent, Int>(
initialState = 0,
presenter = CounterPresenter(),
)

private class CounterPresenter : MoleculePresenter<CounterEvent, Int> {
@Composable
override fun MoleculePresenterScope<CounterEvent>.present(lastState: Int): Int {
var count by remember { mutableIntStateOf(lastState) }
CollectEvents { event ->
when (event) {
CounterEvent.Increment -> count++
}
}
return count
}
}
Loading