diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt index 0d4185c4..f0d29865 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt @@ -29,7 +29,6 @@ class CreatorChannelCommunityDetailReplyUiContractTest { val reply = inflateView(R.layout.activity_creator_channel_community_reply) assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_back)) - assertNotNull(detail.findViewById(R.id.tv_creator_channel_community_detail_title)) assertNotNull(detail.findViewById(R.id.tv_creator_channel_community_detail_comment_header)) assertNotNull(detail.findViewById(R.id.rv_creator_channel_community_detail_comments)) assertNotNull(detail.findViewById(R.id.layout_creator_channel_community_detail_paywall)) @@ -37,14 +36,17 @@ class CreatorChannelCommunityDetailReplyUiContractTest { assertNotNull(detail.findViewById(R.id.layout_creator_channel_community_detail_input_bar)) assertNotNull(detail.findViewById(R.id.et_creator_channel_community_detail_comment)) assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_send)) + assertNotNull(detail.findViewById(R.id.btn_creator_channel_community_detail_edit_cancel)) + assertRemovedId("tv_creator_channel_community_detail_title") assertRemovedId("tv_creator_channel_community_detail_comment_unavailable") assertNotNull(reply.findViewById(R.id.btn_creator_channel_community_reply_back)) - assertNotNull(reply.findViewById(R.id.tv_creator_channel_community_reply_title)) assertNotNull(reply.findViewById(R.id.rv_creator_channel_community_reply)) assertNotNull(reply.findViewById(R.id.layout_creator_channel_community_reply_input_bar)) assertNotNull(reply.findViewById(R.id.et_creator_channel_community_reply)) assertNotNull(reply.findViewById(R.id.btn_creator_channel_community_reply_send)) + assertNotNull(reply.findViewById(R.id.btn_creator_channel_community_reply_edit_cancel)) + assertRemovedId("tv_creator_channel_community_reply_title") } @Test @@ -62,13 +64,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest { 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(R.id.tv_creator_channel_community_more_edit)) + assertNotNull(popup.findViewById(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 fun `커뮤니티 상세 답글 문자열은 한국어 영어 일본어에 존재한다`() { listOf("values", "values-en", "values-ja").forEach { valuesDir -> 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_reply_placeholder\"")) assertTrue(strings.contains("name=\"creator_channel_community_comment_header\"")) @@ -89,6 +101,65 @@ class CreatorChannelCommunityDetailReplyUiContractTest { 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 { val context = ApplicationProvider.getApplicationContext() return LayoutInflater.from(context).inflate(layoutId, null, false) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt index 72a618b0..3eeb4b41 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt @@ -277,6 +277,65 @@ class CreatorChannelCommunityDetailViewModelTest { 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 { 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 fun `댓글 불가 게시물은 댓글 조회와 작성을 호출하지 않고 입력 숨김 상태를 노출한다`() { stubDetail(detailResponse(isCommentAvailable = false)) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityReplyViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityReplyViewModelTest.kt index eea36f46..4faf6672 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityReplyViewModelTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityReplyViewModelTest.kt @@ -226,6 +226,72 @@ class CreatorChannelCommunityReplyViewModelTest { 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 { 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 fun `답글 수정과 삭제는 legacy modifyComment를 호출하고 성공 시 답글을 새로고침한다`() { whenever( @@ -306,6 +372,28 @@ class CreatorChannelCommunityReplyViewModelTest { 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 { commentId == COMMENT_ID && comment == "수정 부모 댓글" && isActive == null }, + org.mockito.kotlin.eq(AUTH_TOKEN) + ) + } + @Test fun `부모 댓글 삭제는 legacy modifyComment를 호출하고 성공 시 삭제 이벤트만 노출한다`() { stubReplies(repliesResponse(ids = listOf(101L), hasNext = false))