Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ dependencies {
androidTestImplementation(testFixtures(projects.core.ui))
androidTestImplementation(projects.feature.afternote.domain)
androidTestImplementation(testFixtures(projects.feature.afternote.domain))
androidTestImplementation(testFixtures(projects.feature.afternote.presentation))
androidTestImplementation(projects.feature.mindrecord.domain)
androidTestImplementation(testFixtures(projects.feature.mindrecord.domain))
androidTestImplementation(projects.feature.receiver.domain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.model.author.ListItem
import com.afternote.feature.afternote.domain.testing.FakeAfternoteRepository
import com.afternote.feature.afternote.presentation.home.AfternoteHomeEntry
import com.afternote.feature.afternote.presentation.home.AfternoteHomeViewModel
import com.afternote.feature.afternote.presentation.testing.AfternoteHomeScreenFixture
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
Expand Down Expand Up @@ -66,17 +65,16 @@ class AfternoteAuthorExtendedAndroidTest {
listFlows[null] =
Pager(PagingConfig(pageSize = 20)) { pagingSource }.flow
listFlows[AfternoteType.SOCIAL_NETWORK] = flowOf(PagingData.empty())
val viewModel = AfternoteHomeViewModel(repository, FakeErrorReporter())
val homeFixture = AfternoteHomeScreenFixture(repository, FakeErrorReporter())
val detailRoutes = mutableListOf<Long>()
val addRoutes = mutableListOf<AfternoteType?>()

composeRule.setContent {
AfternoteTheme {
AfternoteHomeEntry(
homeFixture.Content(
navigateToDetail = detailRoutes::add,
navigateToAdd = addRoutes::add,
onSettingClick = {},
viewModel = viewModel,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.model.author.ListItem
import com.afternote.feature.afternote.domain.testing.FakeAfternoteRepository
import com.afternote.feature.afternote.presentation.home.AfternoteHomeEntry
import com.afternote.feature.afternote.presentation.home.AfternoteHomeViewModel
import com.afternote.feature.afternote.presentation.testing.AfternoteHomeScreenFixture
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -78,15 +77,14 @@ class AfternoteHomeRefreshBannerAndroidTest {
}.flow
}
}
val viewModel = AfternoteHomeViewModel(repository, FakeErrorReporter())
val homeFixture = AfternoteHomeScreenFixture(repository, FakeErrorReporter())

composeRule.setContent {
AfternoteTheme {
AfternoteHomeEntry(
homeFixture.Content(
navigateToDetail = {},
navigateToAdd = {},
onSettingClick = {},
viewModel = viewModel,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ import com.afternote.core.ui.Route
import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.model.LeaveMessageBlock
import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailRoute
import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailViewModel
import com.afternote.feature.afternote.presentation.receiver.navigation.ReceivedAfternoteRoute
import com.afternote.feature.afternote.presentation.testing.ReceivedAfternoteDetailScreenFixture
import com.afternote.feature.home.presentation.HomeTabActions
import com.afternote.feature.home.presentation.receiver.ReceiverHomeEvent
import com.afternote.feature.mindrecord.domain.repository.WeeklyReportRepository
Expand Down Expand Up @@ -560,20 +558,19 @@ class ReceiverRuntimeCompletionAndroidTest {
),
),
)
val viewModel =
ReceivedAfternoteDetailViewModel(
route = ReceivedAfternoteRoute.DetailRoute(afternoteId = 202L),
val detailFixture =
ReceivedAfternoteDetailScreenFixture(
afternoteId = 202L,
receiverRepository = repository,
errorReporter = FakeErrorReporter(),
)

composeRule.setContent {
AfternoteTheme {
ReceivedAfternoteDetailRoute(
detailFixture.Content(
onNavigateBack = {},
onNavigateToFullList = {},
onNavigateToPlaylist = {},
viewModel = viewModel,
)
}
}
Expand Down Expand Up @@ -610,21 +607,20 @@ class ReceiverRuntimeCompletionAndroidTest {
),
),
)
val viewModel =
ReceivedAfternoteDetailViewModel(
route = ReceivedAfternoteRoute.DetailRoute(afternoteId = 303L),
val detailFixture =
ReceivedAfternoteDetailScreenFixture(
afternoteId = 303L,
receiverRepository = repository,
errorReporter = FakeErrorReporter(),
)
val playlistRoutes = mutableListOf<Long>()

composeRule.setContent {
AfternoteTheme {
ReceivedAfternoteDetailRoute(
detailFixture.Content(
onNavigateBack = {},
onNavigateToFullList = {},
onNavigateToPlaylist = playlistRoutes::add,
viewModel = viewModel,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ import com.afternote.afternote_fe.test.FakeErrorReporter
import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.model.LeaveMessageBlock
import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailRoute
import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailViewModel
import com.afternote.feature.afternote.presentation.receiver.navigation.ReceivedAfternoteRoute
import com.afternote.feature.afternote.presentation.testing.ReceivedAfternoteDetailScreenFixture
import com.afternote.feature.receiver.domain.model.ReceivedAccountCredentials
import com.afternote.feature.receiver.domain.model.ReceivedAfternoteDetail
import com.afternote.feature.receiver.domain.model.ReceivedExportBundle
Expand Down Expand Up @@ -177,20 +175,19 @@ class ReceiverAdvancedAndroidTest {
}
detailResults.addLast(Result.failure(IllegalStateException("offline")))
detailResults.addLast(Result.success(receivedSocialDetail()))
val viewModel =
ReceivedAfternoteDetailViewModel(
route = ReceivedAfternoteRoute.DetailRoute(afternoteId = 91L),
val detailFixture =
ReceivedAfternoteDetailScreenFixture(
afternoteId = 91L,
receiverRepository = repository,
errorReporter = FakeErrorReporter(),
)

composeRule.setContent {
AfternoteTheme {
ReceivedAfternoteDetailRoute(
detailFixture.Content(
onNavigateBack = {},
onNavigateToFullList = {},
onNavigateToPlaylist = {},
viewModel = viewModel,
)
}
}
Expand Down
10 changes: 10 additions & 0 deletions feature/afternote/presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins {
android {
namespace = "com.afternote.feature.afternote.presentation"
resourcePrefix = "afternote_"
testFixtures.enable = true
experimentalProperties["android.experimental.enableScreenshotTest"] = true
buildFeatures {
buildConfig = true
Expand Down Expand Up @@ -41,6 +42,15 @@ dependencies {
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.core.ktx)

// 앱 기기 테스트는 fixture를 통해 실제 화면을 열며 production ViewModel을 공개하지 않는다.
testFixturesImplementation(platform(libs.androidx.compose.bom))
testFixturesImplementation(libs.androidx.compose.runtime)
testFixturesImplementation(projects.feature.afternote.domain)
testFixturesImplementation(projects.feature.receiver.domain)
testFixturesImplementation(projects.core.common)
testFixturesImplementation(projects.core.ui)
testFixturesImplementation(libs.androidx.navigation3.runtime)

testImplementation(libs.coroutines.test)
testImplementation(testFixtures(projects.core.domain))
testImplementation(testFixtures(projects.feature.afternote.domain))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.afternote.feature.afternote.presentation

import com.afternote.core.ui.mvi.MviIntent
import com.afternote.core.ui.mvi.ReducerEvent
import com.afternote.core.ui.mvi.UiState

internal data class AfternoteHostUiState(
val isPasskeyRegistered: Boolean? = null,
) : UiState

internal sealed interface AfternoteHostIntent : MviIntent {
data object ObserveProfile : AfternoteHostIntent

data object StopObservingProfile : AfternoteHostIntent
}

internal sealed interface AfternoteHostReducerEvent : ReducerEvent {
data class PasskeyRegistrationChanged(
val registered: Boolean,
) : AfternoteHostReducerEvent
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.afternote.feature.afternote.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.afternote.core.domain.repository.UserProfileCacheRepository
import com.afternote.core.ui.mvi.MviViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
Expand All @@ -17,19 +17,46 @@ import javax.inject.Inject
* 에디터 flow 상태는 flow-scoped
* [com.afternote.feature.afternote.presentation.editor.AfternoteEditorViewModel]이 담당한다.
* 본 ViewModel은 애프터노트 로컬 스택 전체에서 공유하는 사용자 상태만 보유한다.
* 프로필 구독은 마지막 화면이 떠난 뒤 기존 WhileSubscribed와 같은 5초 유예를 둔다.
*/
@HiltViewModel
class AfternoteHostViewModel
internal class AfternoteHostViewModel
@Inject
constructor(
userProfileRepository: UserProfileCacheRepository,
) : ViewModel() {
val isPasskeyRegistered: StateFlow<Boolean?> =
userProfileRepository
.isPasskeyRegisteredFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
private val userProfileRepository: UserProfileCacheRepository,
) : MviViewModel<AfternoteHostIntent, AfternoteHostUiState, AfternoteHostReducerEvent>(AfternoteHostUiState()) {
private var profileJob: Job? = null
private var stopJob: Job? = null

override fun onIntent(intent: AfternoteHostIntent) {
when (intent) {
AfternoteHostIntent.ObserveProfile -> {
stopJob?.cancel()
if (profileJob?.isActive == true) return
profileJob =
viewModelScope.launch {
userProfileRepository.isPasskeyRegisteredFlow().collect {
dispatch(AfternoteHostReducerEvent.PasskeyRegistrationChanged(it))
}
}
}

AfternoteHostIntent.StopObservingProfile -> {
stopJob?.cancel()
stopJob =
viewModelScope.launch {
delay(5_000)
profileJob?.cancel()
}
}
}
}

override fun reduce(
state: AfternoteHostUiState,
event: AfternoteHostReducerEvent,
): AfternoteHostUiState =
when (event) {
is AfternoteHostReducerEvent.PasskeyRegistrationChanged -> state.copy(isPasskeyRegistered = event.registered)
}
}
Loading
Loading