test(creator): 채널 홈 후속 동작을 검증한다
This commit is contained in:
@@ -46,26 +46,122 @@ class CreatorChannelActivitySourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `follow notify source는 미팔로우 직접 팔로우와 팔로우 중 알림 sheet를 연결한다`() {
|
||||
fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||
|
||||
assertTrue(source.contains("CreatorFollowNotifyFragment"))
|
||||
assertFalse(source.contains("CreatorFollowNotifyFragment"))
|
||||
assertTrue(layout.contains("android:id=\"@+id/layout_follow_capsule\""))
|
||||
assertTrue(layout.contains("android:visibility=\"gone\""))
|
||||
assertTrue(source.contains("binding.layoutFollowCapsule.setOnClickListener"))
|
||||
assertTrue(source.contains("binding.ivBell.setOnClickListener"))
|
||||
assertTrue(source.contains("private fun onFollowActionClicked"))
|
||||
assertTrue(source.contains("private fun onFollowCapsuleClicked"))
|
||||
assertTrue(source.contains("private fun onBellClicked"))
|
||||
assertTrue(source.contains("if (!header.isFollow)"))
|
||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
|
||||
assertTrue(source.contains("showFollowNotifyFragment"))
|
||||
assertTrue(source.contains("onClickNotifyAll = { homeActionDelegate?.follow(follow = true, notify = true) }"))
|
||||
assertTrue(source.contains("onClickNotifyNone = { homeActionDelegate?.follow(follow = true, notify = false) }"))
|
||||
assertTrue(source.contains("onClickUnFollow = { homeActionDelegate?.follow(follow = false, notify = false) }"))
|
||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = !header.isNotify)"))
|
||||
assertTrue(source.contains("homeActionDelegate?.follow(follow = false, notify = false)"))
|
||||
assertFalse(source.contains("showFollowNotifyFragment"))
|
||||
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
|
||||
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
|
||||
assertTrue(source.contains("binding.ivBell.isEnabled = titleBarState.isActionEnabled"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 11 more source는 기존 프로필 메뉴 의미를 BottomSheet로 연결한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val sheet = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt"
|
||||
).readText()
|
||||
val layout = projectFile("app/src/main/res/layout/dialog_creator_channel_more.xml").readText()
|
||||
|
||||
assertTrue(source.contains("CreatorChannelMoreBottomSheet.newInstance"))
|
||||
assertTrue(source.contains("showUserReportDialog"))
|
||||
assertTrue(source.contains("showProfileReportDialog"))
|
||||
assertTrue(source.contains("homeActionDelegate?.blockUser"))
|
||||
assertFalse(source.contains("creator_channel_more_ready"))
|
||||
assertTrue(sheet.contains("BottomSheetDialogFragment"))
|
||||
assertTrue(sheet.contains("onClickBlock"))
|
||||
assertFalse(sheet.contains("onClickUnblock"))
|
||||
assertFalse(sheet.contains("isBlocked"))
|
||||
assertTrue(sheet.contains("onClickUserReport"))
|
||||
assertTrue(sheet.contains("onClickProfileReport"))
|
||||
assertTrue(layout.contains("@+id/tv_user_block"))
|
||||
assertTrue(layout.contains("@string/menu_user_block"))
|
||||
assertTrue(layout.contains("@+id/tv_user_report"))
|
||||
assertTrue(layout.contains("@string/menu_user_report"))
|
||||
assertTrue(layout.contains("@+id/tv_profile_report"))
|
||||
assertTrue(layout.contains("@string/menu_profile_report"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 11 owner source는 본인 페이지 상단 타인 액션을 숨긴다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val dto = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt"
|
||||
).readText()
|
||||
val uiModel = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt"
|
||||
).readText()
|
||||
val mapper = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt"
|
||||
).readText()
|
||||
|
||||
assertFalse(dto.contains("@SerializedName(\"isOwner\")"))
|
||||
assertFalse(dto.contains("@SerializedName(\"isBlock\")"))
|
||||
assertTrue(uiModel.contains("val isOwner: Boolean"))
|
||||
assertFalse(uiModel.contains("val isBlock: Boolean"))
|
||||
assertTrue(mapper.contains("fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long)"))
|
||||
assertTrue(mapper.contains("isOwner = creator.creatorId == currentMemberId"))
|
||||
assertTrue(source.contains("if (header.isOwner)"))
|
||||
assertTrue(source.contains("binding.layoutFollowCapsule.visibility = View.GONE"))
|
||||
assertTrue(source.contains("binding.ivBell.visibility = View.GONE"))
|
||||
assertTrue(source.contains("binding.ivMore.visibility = View.GONE"))
|
||||
assertTrue(source.contains("isChatVisible = !header.isOwner"))
|
||||
assertTrue(source.contains("CreatorChannelMoreBottomSheet.newInstance()"))
|
||||
assertFalse(source.contains("onClickUnblock"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 11 owner DM source는 MainV2 채팅 DM 필터로 진입한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||
val main = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
|
||||
val chat = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt").readText()
|
||||
val filter = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt").readText()
|
||||
val strings = projectFile("app/src/main/res/values/strings.xml").readText()
|
||||
|
||||
assertTrue(strings.contains("name=\"creator_channel_dm_check_button\">DM 확인하기"))
|
||||
assertTrue(layout.contains("android:id=\"@+id/tv_chat_button\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/tv_dm_button\""))
|
||||
assertTrue(layout.contains("tools:visibility=\"visible\""))
|
||||
assertTrue(source.contains("MainV2Activity.newChatDmIntent(this)"))
|
||||
assertTrue(
|
||||
source.contains(
|
||||
"if (header.isOwner) R.string.creator_channel_dm_check_button else R.string.creator_channel_dm_button"
|
||||
)
|
||||
)
|
||||
assertTrue(source.contains("isDmVisible = header.isOwner || header.isDmAvailable"))
|
||||
assertTrue(main.contains("fun newChatDmIntent(context: Context): Intent"))
|
||||
assertTrue(main.contains("Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP"))
|
||||
assertTrue(main.contains("putExtra(EXTRA_CHAT_FILTER, ChatRoomFilter.DM.name)"))
|
||||
assertTrue(main.contains("openChatWithInitialFilter"))
|
||||
assertTrue(main.contains("ChatMainFragment.newInstance(initialChatFilter)"))
|
||||
assertTrue(chat.contains("fun newInstance(initialFilter: ChatRoomFilter? = null): ChatMainFragment"))
|
||||
assertTrue(chat.contains("selectedIndex = initialFilter.tabIndex"))
|
||||
assertTrue(chat.contains("fun selectFilter(filter: ChatRoomFilter)"))
|
||||
assertTrue(filter.contains("DM(\"DM\", 2)"))
|
||||
assertTrue(source.contains("DmChatRoomActivity.newIntentByCreatorId(this, creatorId)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 10 컨테이너 layout은 TabLayout과 ViewPager2를 가진다`() {
|
||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||
@@ -190,6 +286,37 @@ class CreatorChannelActivitySourceTest {
|
||||
assertTrue(source.contains("Color.TRANSPARENT"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 11 title bar는 sticky black 상태에서 닉네임을 표시한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||
|
||||
assertTrue(layout.contains("@+id/tv_title_nickname"))
|
||||
assertTrue(layout.contains("style=\"@style/Typography.Heading2\""))
|
||||
assertTrue(layout.contains("android:ellipsize=\"end\""))
|
||||
assertTrue(layout.contains("android:maxLines=\"1\""))
|
||||
assertTrue(layout.contains("app:layout_constraintStart_toEndOf=\"@id/iv_back\""))
|
||||
assertTrue(layout.contains("app:layout_constraintEnd_toStartOf=\"@id/title_action_container\""))
|
||||
assertTrue(source.contains("binding.tvTitleNickname.text = header.nickname"))
|
||||
assertTrue(source.contains("binding.tvTitleNickname.isVisible = shouldUseBlackTitleBar"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 11 tab bar typography는 선택 비선택 모두 16sp medium을 사용한다`() {
|
||||
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
|
||||
val typography = projectFile("app/src/main/res/values/typography.xml").readText()
|
||||
|
||||
assertTrue(layout.contains("app:tabTextAppearance=\"@style/CreatorChannelTabText\""))
|
||||
assertTrue(typography.contains("<style name=\"CreatorChannelTabText\" parent=\"Typography.Body2\">"))
|
||||
assertTrue(typography.contains("<item name=\"android:fontFamily\">@font/medium</item>"))
|
||||
assertTrue(typography.contains("<item name=\"android:textSize\">16sp</item>"))
|
||||
assertTrue(typography.contains("<item name=\"android:letterSpacing\">0</item>"))
|
||||
assertTrue(typography.contains("<item name=\"lineHeight\">23.2sp</item>"))
|
||||
assertFalse(layout.contains("app:tabSelectedTextAppearance"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creator channel home은 어두운 header 위 status bar icon을 밝게 표시한다`() {
|
||||
val source = projectFile(
|
||||
@@ -375,6 +502,22 @@ class CreatorChannelActivitySourceTest {
|
||||
assertFalse(adapter.contains("addActivityRow("))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `활동 summary string resource는 label value row 구조에서 사용하지 않는다`() {
|
||||
val stringFiles = listOf(
|
||||
"app/src/main/res/values/strings.xml",
|
||||
"app/src/main/res/values-en/strings.xml",
|
||||
"app/src/main/res/values-ja/strings.xml"
|
||||
)
|
||||
|
||||
stringFiles.forEach { path ->
|
||||
assertFalse(
|
||||
"$path contains unused creator_channel_activity_summary",
|
||||
projectFile(path).readText().contains("creator_channel_activity_summary")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SNS 섹션은 전용 layout 아이콘 row와 안전한 링크 실행을 유지한다`() {
|
||||
val adapter = projectFile(
|
||||
|
||||
@@ -26,7 +26,7 @@ class CreatorChannelHomeMapperTest {
|
||||
|
||||
@Test
|
||||
fun `크리에이터 정보와 탭 순서를 UI content에 그대로 매핑한다`() {
|
||||
val content = response().toUiContent()
|
||||
val content = response().toUiContent(currentMemberId = 100L)
|
||||
|
||||
assertEquals(100L, content.header.creatorId)
|
||||
assertEquals(200L, content.header.characterId)
|
||||
@@ -37,6 +37,7 @@ class CreatorChannelHomeMapperTest {
|
||||
assertFalse(content.header.isNotify)
|
||||
assertTrue(content.header.isAiChatAvailable)
|
||||
assertTrue(content.header.isDmAvailable)
|
||||
assertTrue(content.header.isOwner)
|
||||
assertEquals(
|
||||
listOf(
|
||||
R.string.creator_channel_tab_home,
|
||||
@@ -71,7 +72,7 @@ class CreatorChannelHomeMapperTest {
|
||||
youtubeUrl = "",
|
||||
kakaoOpenChatUrl = ""
|
||||
)
|
||||
).toUiContent()
|
||||
).toUiContent(currentMemberId = 1L)
|
||||
|
||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.CurrentLive })
|
||||
assertFalse(content.sections.any { it is CreatorChannelHomeSection.LatestAudioContent })
|
||||
@@ -88,7 +89,7 @@ class CreatorChannelHomeMapperTest {
|
||||
|
||||
@Test
|
||||
fun `값이 있는 홈 필드는 section으로 생성하고 blank SNS URL만 제외한다`() {
|
||||
val content = response().toUiContent()
|
||||
val content = response().toUiContent(currentMemberId = 1L)
|
||||
|
||||
assertTrue(content.sections.any { it is CreatorChannelHomeSection.CurrentLive })
|
||||
assertTrue(content.sections.any { it is CreatorChannelHomeSection.LatestAudioContent })
|
||||
@@ -116,7 +117,7 @@ class CreatorChannelHomeMapperTest {
|
||||
youtubeUrl = "https://youtube.example",
|
||||
kakaoOpenChatUrl = "kakao-open-chat"
|
||||
)
|
||||
).toUiContent()
|
||||
).toUiContent(currentMemberId = 1L)
|
||||
|
||||
val sns = content.sections.filterIsInstance<CreatorChannelHomeSection.Sns>().single()
|
||||
|
||||
@@ -140,7 +141,7 @@ class CreatorChannelHomeMapperTest {
|
||||
schedule(id = 1L, scheduledAtUtc = "2026-06-12T10:00:00Z"),
|
||||
schedule(id = 3L, scheduledAtUtc = "2026-06-12T12:00:00Z")
|
||||
)
|
||||
).toUiContent()
|
||||
).toUiContent(currentMemberId = 1L)
|
||||
|
||||
val schedules = content.sections.filterIsInstance<CreatorChannelHomeSection.Schedules>().single()
|
||||
|
||||
|
||||
@@ -155,6 +155,58 @@ class CreatorChannelHomeViewModelTest {
|
||||
assertEquals(false, viewModel.isFollowInProgressLiveData.requireValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로잉 상태 알림 아이콘 액션은 알림 끄기 조합을 호출한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.followCreator(100L, true, false, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, Any(), null))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = true, notify = false)
|
||||
|
||||
verify(repository).followCreator(100L, true, false, "Bearer test-token")
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertTrue(state.header.isFollow)
|
||||
assertEquals(false, state.header.isNotify)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로잉 알림 꺼짐 상태 알림 아이콘 액션은 알림 켜기 조합을 호출한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, response(isNotify = false), null))
|
||||
)
|
||||
whenever(repository.followCreator(100L, true, true, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, Any(), null))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = true, notify = true)
|
||||
|
||||
verify(repository).followCreator(100L, true, true, "Bearer test-token")
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertTrue(state.header.isFollow)
|
||||
assertTrue(state.header.isNotify)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로잉 상태 capsule 액션은 팔로우 취소 조합을 호출한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.followCreator(100L, false, false, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, Any(), null))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = false, notify = false)
|
||||
|
||||
verify(repository).followCreator(100L, false, false, "Bearer test-token")
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertEquals(false, state.header.isFollow)
|
||||
assertEquals(false, state.header.isNotify)
|
||||
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||
assertEquals(R.string.creator_channel_unfollow_success, toastEvent?.consume()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로우 실패는 현재 content를 유지하고 unknown toast만 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
@@ -217,6 +269,49 @@ class CreatorChannelHomeViewModelTest {
|
||||
assertEquals(null, toastEvent?.consume())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `차단 성공은 block API를 호출하고 차단 완료 토스트를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.blockUser(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.blockUser()
|
||||
|
||||
verify(repository).blockUser(100L, "Bearer test-token")
|
||||
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||
assertEquals(R.string.creator_channel_block_success, toastEvent?.consume()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `사용자 신고 실패는 접수 완료 대신 unknown toast를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.reportUser(100L, "reason", "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(false, null, "failed"))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.reportUser("reason")
|
||||
|
||||
verify(repository).reportUser(100L, "reason", "Bearer test-token")
|
||||
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||
assertEquals(R.string.common_error_unknown, toastEvent?.consume()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `사용자 신고 throwable은 접수 완료 대신 unknown toast를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.reportUser(100L, "reason", "Bearer test-token")).thenReturn(
|
||||
Single.error(IllegalStateException("network"))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.reportUser("reason")
|
||||
|
||||
verify(repository).reportUser(100L, "reason", "Bearer test-token")
|
||||
val toastEvent = viewModel.toastLiveData.requireValue()
|
||||
assertEquals(R.string.common_error_unknown, toastEvent?.consume()?.resId)
|
||||
}
|
||||
|
||||
private fun setImmediateRxSchedulers() {
|
||||
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
|
||||
RxJavaPlugins.setIoSchedulerHandler(trampoline)
|
||||
@@ -224,7 +319,10 @@ class CreatorChannelHomeViewModelTest {
|
||||
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||
}
|
||||
|
||||
private fun response(isFollow: Boolean = true) = CreatorChannelHomeResponse(
|
||||
private fun response(
|
||||
isFollow: Boolean = true,
|
||||
isNotify: Boolean = false
|
||||
) = CreatorChannelHomeResponse(
|
||||
creator = CreatorChannelCreatorResponse(
|
||||
creatorId = 100L,
|
||||
characterId = 200L,
|
||||
@@ -234,7 +332,7 @@ class CreatorChannelHomeViewModelTest {
|
||||
isAiChatAvailable = true,
|
||||
isDmAvailable = true,
|
||||
isFollow = isFollow,
|
||||
isNotify = false
|
||||
isNotify = isNotify
|
||||
),
|
||||
currentLive = CreatorChannelLiveResponse(1L, "라이브", null, "2026-06-11T12:00:00Z", 0, false),
|
||||
latestAudioContent = CreatorChannelAudioContentResponse(1L, "오디오", null, null, 0, true, false, null, null),
|
||||
|
||||
@@ -101,7 +101,7 @@ class ChatMainFragmentLayoutTest {
|
||||
assertTrue(source.contains("R.string.screen_chat_filter_all"))
|
||||
assertTrue(source.contains("R.string.screen_chat_filter_ai"))
|
||||
assertTrue(source.contains("R.string.screen_chat_filter_dm"))
|
||||
assertTrue(source.contains("viewModel.loadFirstPage()"))
|
||||
assertTrue(source.contains("viewModel.loadFirstPage(initialFilter ?: ChatRoomFilter.ALL)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user