Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138package social.grain.support
import androidx.compose.foundation.text.input.TextFieldStateimport androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEndimport android.content.Contextimport android.os.Looperimport androidx.test.core.app.ApplicationProviderimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.firstimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withTimeoutOrNullimport social.grain.data.api.DPoPimport social.grain.data.auth.AuthManagerimport social.grain.data.auth.SecureStoreimport social.grain.data.auth.StoredAccountimport social.grain.data.auth.TokenStorage
val appContext: Context get() = ApplicationProvider.getApplicationContext()
/** * Everything a view model under test needs, wired to a [StubServer]. * * The auth manager is the real one against real Keystore-backed storage rather * than a stand-in, because a view model's most interesting branch is usually * "am I signed in" — and a fake that always says yes hides the signed-out path * that every screen has to handle. */class Harness( val server: StubServer = StubServer(), signedIn: Boolean = true, val did: String = "did:plc:me", val handle: String = "me.grain.social", avatar: String? = "https://cdn.grain.test/avatar/me.jpg",) { val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val storage = TokenStorage(SecureStore(appContext))
init { FakeAndroidKeyStore.install() // Whatever a previous test left behind is not this test's session. storage.accounts.forEach { storage.removeAccount(it.did) } storage.activeDid = null
if (signedIn) { storage.storeDpopKey(did, DPoP.createEphemeral().encodedKeyPair()) storage.storeTokens( did = did, accessToken = "test-access", refreshToken = "test-refresh", handle = handle, expiresAtMillis = System.currentTimeMillis() + 3_600_000, scope = null, ) storage.setAvatar(did, avatar) storage.activeDid = did storage.upsertAccount(StoredAccount(did, handle, avatar)) } }
val auth = AuthManager(storage, server.client(), testJson, scope) val client = server.xrpc()
val feedCache = social.grain.data.local.FeedCache(appContext, testJson) val viewedStories = social.grain.data.local.ViewedStoryStore(appContext, testJson, scope) val storyStatus = social.grain.data.local.StoryStatusCache() val recentSearches = social.grain.data.local.RecentSearchStore(appContext, testJson) val labelDefinitions = social.grain.data.local.LabelDefinitionsCache(client, auth, scope)
/** Undo everything this harness put on the device. */ fun tearDown() { storage.accounts.forEach { storage.removeAccount(it.did) } storage.activeDid = null scope.coroutineContext[kotlinx.coroutines.Job]?.cancel() server.shutdown() }}
/** * Block until the flow's value satisfies [predicate]. * * Real time rather than a virtual scheduler: the client under test hands its * requests to OkHttp's own dispatcher, which no test scheduler controls, so * skipping delay would just mean giving up before the reply arrives. */fun <T> StateFlow<T>.awaitValue( what: String = "state", timeoutMs: Long = 10_000, predicate: (T) -> Boolean,): T = runBlocking { withTimeoutOrNull(timeoutMs) { first(predicate) } ?: throw AssertionError("Timed out waiting for $what. Last value: ${this@awaitValue.value}")}
/** Block until [nsid] has been called at least [count] times. */fun StubServer.awaitCall(nsid: String, count: Int = 1, timeoutMs: Long = 10_000): StubCall { val deadline = System.currentTimeMillis() + timeoutMs while (System.currentTimeMillis() < deadline) { if (countOf(nsid) >= count) return lastCall(nsid) Thread.sleep(10) } throw AssertionError( "Timed out waiting for $count call(s) to $nsid; saw ${countOf(nsid)}", )}
/** * Let anything already queued on the main thread run to completion. * * Used where a test has to prove a *negative* — that no request went out — * which has no state change to wait on. The sleep is what gives work already * handed to a background dispatcher a chance to arrive. */fun settle() { org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle() Thread.sleep(120) org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle()}
/** * Publish pending snapshot writes so `snapshotFlow` collectors see them. * * Compose normally does this from the recomposer's frame loop. A view model * exercised without a composition has no frame loop, so a `TextFieldState` * edited from a test would otherwise never reach the flow watching it. */fun applySnapshot() { androidx.compose.runtime.snapshots.Snapshot.sendApplyNotifications()}
/** Type [text] into a field a view model owns, as the text field would. */fun TextFieldState.type(text: String) { setTextAndPlaceCursorAtEnd(text) applySnapshot()}