test(creator-channel): 댓글 답글 후속 계약을 검증한다

This commit is contained in:
2026-07-09 01:28:48 +09:00
parent 3271c9e142
commit 89f3511864
3 changed files with 222 additions and 4 deletions

View File

@@ -29,7 +29,6 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
val reply = inflateView(R.layout.activity_creator_channel_community_reply) val reply = inflateView(R.layout.activity_creator_channel_community_reply)
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back)) assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back))
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_title))
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header)) assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header))
assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments)) assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments))
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall)) assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall))
@@ -37,14 +36,17 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_input_bar)) assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_input_bar))
assertNotNull(detail.findViewById<EditText>(R.id.et_creator_channel_community_detail_comment)) assertNotNull(detail.findViewById<EditText>(R.id.et_creator_channel_community_detail_comment))
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send)) assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel))
assertRemovedId("tv_creator_channel_community_detail_title")
assertRemovedId("tv_creator_channel_community_detail_comment_unavailable") assertRemovedId("tv_creator_channel_community_detail_comment_unavailable")
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back)) assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back))
assertNotNull(reply.findViewById<TextView>(R.id.tv_creator_channel_community_reply_title))
assertNotNull(reply.findViewById<RecyclerView>(R.id.rv_creator_channel_community_reply)) assertNotNull(reply.findViewById<RecyclerView>(R.id.rv_creator_channel_community_reply))
assertNotNull(reply.findViewById<View>(R.id.layout_creator_channel_community_reply_input_bar)) assertNotNull(reply.findViewById<View>(R.id.layout_creator_channel_community_reply_input_bar))
assertNotNull(reply.findViewById<EditText>(R.id.et_creator_channel_community_reply)) assertNotNull(reply.findViewById<EditText>(R.id.et_creator_channel_community_reply))
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_send)) assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_send))
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_edit_cancel))
assertRemovedId("tv_creator_channel_community_reply_title")
} }
@Test @Test
@@ -62,13 +64,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
assertRemovedId("iv_creator_channel_community_reply_item_heart") assertRemovedId("iv_creator_channel_community_reply_item_heart")
} }
@Test
fun `댓글 답글 더보기 popup layout은 regular font를 사용한다`() {
val popup = inflateView(R.layout.view_creator_channel_community_more_popup)
val popupLayout = projectFile("app/src/main/res/layout/view_creator_channel_community_more_popup.xml").readText()
assertNotNull(popup.findViewById<TextView>(R.id.tv_creator_channel_community_more_edit))
assertNotNull(popup.findViewById<TextView>(R.id.tv_creator_channel_community_more_delete))
assertTrue(popupLayout.contains("android:fontFamily=\"@font/regular\""))
assertTrue(popupLayout.contains("@string/creator_channel_fantalk_edit"))
assertTrue(popupLayout.contains("@string/creator_channel_fantalk_delete"))
}
@Test @Test
fun `커뮤니티 상세 답글 문자열은 한국어 영어 일본어에 존재한다`() { fun `커뮤니티 상세 답글 문자열은 한국어 영어 일본어에 존재한다`() {
listOf("values", "values-en", "values-ja").forEach { valuesDir -> listOf("values", "values-en", "values-ja").forEach { valuesDir ->
val strings = projectFile("app/src/main/res/$valuesDir/strings.xml").readText() val strings = projectFile("app/src/main/res/$valuesDir/strings.xml").readText()
assertTrue(strings.contains("name=\"creator_channel_community_detail_title\""))
assertTrue(strings.contains("name=\"creator_channel_community_reply_title\""))
assertTrue(strings.contains("name=\"creator_channel_community_comment_placeholder\"")) assertTrue(strings.contains("name=\"creator_channel_community_comment_placeholder\""))
assertTrue(strings.contains("name=\"creator_channel_community_reply_placeholder\"")) assertTrue(strings.contains("name=\"creator_channel_community_reply_placeholder\""))
assertTrue(strings.contains("name=\"creator_channel_community_comment_header\"")) assertTrue(strings.contains("name=\"creator_channel_community_comment_header\""))
@@ -89,6 +101,65 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
assertTrue(manifest.contains("android:windowSoftInputMode=\"stateAlwaysHidden|adjustResize\"")) assertTrue(manifest.contains("android:windowSoftInputMode=\"stateAlwaysHidden|adjustResize\""))
} }
@Test
fun `댓글 더보기 메뉴는 수정하기 삭제하기 문구를 사용한다`() {
val ownerMenu = projectFile("app/src/main/res/menu/content_comment_option_menu.xml").readText()
val creatorMenu = projectFile("app/src/main/res/menu/content_comment_option_menu2.xml").readText()
assertTrue(ownerMenu.contains("@string/creator_channel_fantalk_edit"))
assertTrue(ownerMenu.contains("@string/creator_channel_fantalk_delete"))
assertTrue(creatorMenu.contains("@string/creator_channel_fantalk_delete"))
}
@Test
fun `상세와 답글 Activity는 수정 Dialog 대신 입력창 수정 모드와 V2ModalDialog 삭제 확인을 사용한다`() {
val detailActivity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
"CreatorChannelCommunityDetailActivity.kt"
).readText()
val replyActivity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/" +
"CreatorChannelCommunityReplyActivity.kt"
).readText()
assertTrue(detailActivity.contains("V2ModalDialog"))
assertTrue(replyActivity.contains("V2ModalDialog"))
assertTrue(detailActivity.contains("startCommentEdit"))
assertTrue(replyActivity.contains("startReplyEdit"))
assertTrue(replyActivity.contains("startParentCommentEdit"))
assertTrue(detailActivity.contains("btnCreatorChannelCommunityDetailEditCancel"))
assertTrue(replyActivity.contains("btnCreatorChannelCommunityReplyEditCancel"))
assertTrue(detailActivity.contains("showSoftInput"))
assertTrue(detailActivity.contains("hideSoftInputFromWindow"))
assertTrue(replyActivity.contains("showSoftInput"))
assertTrue(replyActivity.contains("hideSoftInputFromWindow"))
assertTrue(replyActivity.contains("replyWrittenEventLiveData.observe"))
assertTrue(!detailActivity.contains("AlertDialog"))
assertTrue(!replyActivity.contains("AlertDialog"))
}
@Test
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
val commentAdapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/ui/" +
"CreatorChannelCommunityCommentAdapter.kt"
).readText()
val replyAdapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/" +
"CreatorChannelCommunityReplyAdapter.kt"
).readText()
val morePopup = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/ui/" +
"CreatorChannelCommunityMorePopup.kt"
).readText()
assertTrue(commentAdapter.contains("CreatorChannelCommunityMorePopup"))
assertTrue(replyAdapter.contains("CreatorChannelCommunityMorePopup"))
assertTrue(!commentAdapter.contains("PopupMenu"))
assertTrue(!replyAdapter.contains("PopupMenu"))
assertTrue(morePopup.contains("ViewCreatorChannelCommunityMorePopupBinding"))
}
private fun inflateView(layoutId: Int): View { private fun inflateView(layoutId: Int): View {
val context = ApplicationProvider.getApplicationContext<Application>() val context = ApplicationProvider.getApplicationContext<Application>()
return LayoutInflater.from(context).inflate(layoutId, null, false) return LayoutInflater.from(context).inflate(layoutId, null, false)

View File

@@ -277,6 +277,65 @@ class CreatorChannelCommunityDetailViewModelTest {
verify(legacyRepository, never()).modifyComment(any(), any()) verify(legacyRepository, never()).modifyComment(any(), any())
} }
@Test
fun `댓글 수정 선택은 하단 입력창 수정 모드로 전환하고 취소 시 입력을 비운다`() {
stubDetail(detailResponse(isCommentAvailable = true))
viewModel.loadDetail(POST_ID)
viewModel.startCommentEdit(COMMENT_ID, "기존 댓글")
val editingState = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
assertEquals("기존 댓글", editingState.commentInput)
assertEquals(COMMENT_ID, editingState.editingCommentId)
viewModel.cancelCommentEdit()
val canceledState = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
assertEquals("", canceledState.commentInput)
assertEquals(null, canceledState.editingCommentId)
}
@Test
fun `댓글 수정 모드 send는 수정 API를 호출하고 성공 시 수정 모드를 해제한다`() {
whenever(repository.getCommunityPostDetail(POST_ID, AUTH_TOKEN))
.thenReturn(
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = true, commentIds = listOf(11L)), null)),
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = true, commentIds = listOf(12L)), null))
)
whenever(legacyRepository.modifyComment(any(), org.mockito.kotlin.eq(AUTH_TOKEN)))
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
viewModel.loadDetail(POST_ID)
viewModel.startCommentEdit(COMMENT_ID, "기존 댓글")
viewModel.updateCommentInput(" 수정 댓글 ")
viewModel.submitComment()
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
assertEquals("", state.commentInput)
assertEquals(null, state.editingCommentId)
assertEquals(listOf(12L), state.comments.map { it.commentId })
verify(legacyRepository).modifyComment(
argThat<ModifyCommentRequest> { commentId == COMMENT_ID && comment == "수정 댓글" && isActive == null },
org.mockito.kotlin.eq(AUTH_TOKEN)
)
}
@Test
fun `댓글 수정 모드 send에서 내용이 같으면 변경 없음 toast를 노출하고 API를 호출하지 않는다`() {
stubDetail(detailResponse(isCommentAvailable = true))
viewModel.loadDetail(POST_ID)
viewModel.startCommentEdit(COMMENT_ID, "기존 댓글")
viewModel.updateCommentInput(" 기존 댓글 ")
viewModel.submitComment()
assertEquals(
kr.co.vividnext.sodalive.R.string.audio_content_comment_no_change,
viewModel.toastLiveData.requireValue()?.consume()?.resId
)
verify(legacyRepository, never()).modifyComment(any(), any())
}
@Test @Test
fun `댓글 불가 게시물은 댓글 조회와 작성을 호출하지 않고 입력 숨김 상태를 노출한다`() { fun `댓글 불가 게시물은 댓글 조회와 작성을 호출하지 않고 입력 숨김 상태를 노출한다`() {
stubDetail(detailResponse(isCommentAvailable = false)) stubDetail(detailResponse(isCommentAvailable = false))

View File

@@ -226,6 +226,72 @@ class CreatorChannelCommunityReplyViewModelTest {
verify(legacyRepository, never()).modifyComment(any(), any()) verify(legacyRepository, never()).modifyComment(any(), any())
} }
@Test
fun `답글 수정 선택은 하단 입력창 수정 모드로 전환하고 취소 시 입력을 비운다`() {
stubReplies(repliesResponse(ids = listOf(101L), hasNext = false))
viewModel.loadReplies(POST_ID, parentComment())
viewModel.startReplyEdit(REPLY_ID, "기존 답글")
val editingState = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content
assertEquals("기존 답글", editingState.replyInput)
assertTrue(editingState.editingTarget != null)
viewModel.cancelReplyEdit()
val canceledState = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content
assertEquals("", canceledState.replyInput)
assertEquals(null, canceledState.editingTarget)
}
@Test
fun `답글 수정 모드 send는 수정 API를 호출하고 성공 시 수정 모드를 해제한다`() {
whenever(
repository.getCommunityCommentReplies(
COMMENT_ID,
0,
CreatorChannelCommunityReplyViewModel.DEFAULT_PAGE_SIZE,
AUTH_TOKEN
)
)
.thenReturn(
Single.just(ApiResponse(true, repliesResponse(ids = listOf(101L), hasNext = false), null)),
Single.just(ApiResponse(true, repliesResponse(ids = listOf(102L), hasNext = false), null))
)
whenever(legacyRepository.modifyComment(any(), org.mockito.kotlin.eq(AUTH_TOKEN)))
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
viewModel.loadReplies(POST_ID, parentComment())
viewModel.startReplyEdit(REPLY_ID, "기존 답글")
viewModel.updateReplyInput(" 수정 답글 ")
viewModel.submitReply()
val state = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content
assertEquals("", state.replyInput)
assertEquals(null, state.editingTarget)
assertEquals(listOf(102L), state.replies.map { it.replyId })
verify(legacyRepository).modifyComment(
argThat<ModifyCommentRequest> { commentId == REPLY_ID && comment == "수정 답글" && isActive == null },
org.mockito.kotlin.eq(AUTH_TOKEN)
)
}
@Test
fun `답글 수정 모드 send에서 내용이 같으면 변경 없음 toast를 노출하고 API를 호출하지 않는다`() {
stubReplies(repliesResponse(ids = listOf(101L), hasNext = false))
viewModel.loadReplies(POST_ID, parentComment())
viewModel.startReplyEdit(REPLY_ID, "기존 답글")
viewModel.updateReplyInput(" 기존 답글 ")
viewModel.submitReply()
assertEquals(
kr.co.vividnext.sodalive.R.string.audio_content_comment_no_change,
viewModel.toastLiveData.requireValue()?.consume()?.resId
)
verify(legacyRepository, never()).modifyComment(any(), any())
}
@Test @Test
fun `답글 수정과 삭제는 legacy modifyComment를 호출하고 성공 시 답글을 새로고침한다`() { fun `답글 수정과 삭제는 legacy modifyComment를 호출하고 성공 시 답글을 새로고침한다`() {
whenever( whenever(
@@ -306,6 +372,28 @@ class CreatorChannelCommunityReplyViewModelTest {
verify(legacyRepository, never()).modifyComment(any(), any()) verify(legacyRepository, never()).modifyComment(any(), any())
} }
@Test
fun `부모 댓글 수정 선택 후 send는 부모 댓글 수정 API를 호출하고 수정 모드를 해제한다`() {
stubReplies(repliesResponse(ids = listOf(101L), hasNext = false))
whenever(legacyRepository.modifyComment(any(), org.mockito.kotlin.eq(AUTH_TOKEN)))
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
viewModel.loadReplies(POST_ID, parentComment())
viewModel.startParentCommentEdit("부모 댓글")
viewModel.updateReplyInput(" 수정 부모 댓글 ")
viewModel.submitReply()
val state = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content
assertEquals("수정 부모 댓글", state.parentComment.comment)
assertEquals("", state.replyInput)
assertEquals(null, state.editingTarget)
assertTrue(viewModel.parentCommentChangedEventLiveData.requireValue() == true)
verify(legacyRepository).modifyComment(
argThat<ModifyCommentRequest> { commentId == COMMENT_ID && comment == "수정 부모 댓글" && isActive == null },
org.mockito.kotlin.eq(AUTH_TOKEN)
)
}
@Test @Test
fun `부모 댓글 삭제는 legacy modifyComment를 호출하고 성공 시 삭제 이벤트만 노출한다`() { fun `부모 댓글 삭제는 legacy modifyComment를 호출하고 성공 시 삭제 이벤트만 노출한다`() {
stubReplies(repliesResponse(ids = listOf(101L), hasNext = false)) stubReplies(repliesResponse(ids = listOf(101L), hasNext = false))