From 40ef5710fb5bbf21b342c824a12eac18c9ff8c9b Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 22 Jun 2026 17:46:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20FanTalk=20=ED=83=AD=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=97=B0=EA=B2=B0=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../creator/channel/CreatorChannelActivity.kt | 47 ++++- .../channel/CreatorChannelPagerAdapter.kt | 2 + .../CreatorChannelActivitySourceTest.kt | 44 +++- .../channel/CreatorChannelPagerAdapterTest.kt | 7 +- ...CreatorChannelFanTalkFragmentLayoutTest.kt | 190 ++++++++++++++++++ 5 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt index 6bae2801..b9db801a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt @@ -63,6 +63,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioConte import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragment import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelScrollState @@ -83,7 +84,8 @@ class CreatorChannelActivity : CreatorChannelLiveFragment.Host, CreatorChannelAudioFragment.Host, CreatorChannelSeriesFragment.Host, - CreatorChannelCommunityFragment.Host { + CreatorChannelCommunityFragment.Host, + CreatorChannelFanTalkFragment.Host { private val liveViewModel: LiveViewModel by inject() private val myPageViewModel: MyPageViewModel by inject() @@ -430,6 +432,9 @@ class CreatorChannelActivity : CreatorChannelTab.Community.ordinal -> binding.viewPager.post { findCommunityFragment()?.onCreatorChannelCommunityTabSelected() } + CreatorChannelTab.FanTalk.ordinal -> binding.viewPager.post { + findFanTalkFragment()?.onCreatorChannelFanTalkTabSelected() + } } } } @@ -458,6 +463,11 @@ class CreatorChannelActivity : findCommunityFragment()?.onCreatorChannelCommunityTabSelected() } } + if (binding.viewPager.currentItem == CreatorChannelTab.FanTalk.ordinal) { + binding.viewPager.post { + findFanTalkFragment()?.onCreatorChannelFanTalkTabSelected() + } + } } override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) { @@ -532,6 +542,27 @@ class CreatorChannelActivity : } } + override fun onCreatorChannelFanTalkContentChanged() { + updateViewPagerHeight { + postCheckCreatorChannelCurrentTabNeedsMore() + } + } + + override fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long) { + SodaDialog( + activity = this, + layoutInflater = layoutInflater, + title = getString(R.string.screen_user_profile_cheer_delete_title), + desc = getString(R.string.screen_user_profile_cheer_delete_desc), + confirmButtonTitle = getString(R.string.confirm_delete_title), + confirmButtonClick = { + findFanTalkFragment()?.onCreatorChannelFanTalkDeleteConfirmed(fanTalkId) + }, + cancelButtonTitle = getString(R.string.cancel), + cancelButtonClick = {} + ).show(screenWidth) + } + override fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) { CreatorCommunityPostMenuBottomSheetDialog( isFixed = item.isPinned, @@ -623,6 +654,10 @@ class CreatorChannelActivity : findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested() } + private fun refreshCreatorChannelFanTalk() { + findFanTalkFragment()?.onCreatorChannelFanTalkRefreshRequested() + } + private fun setupOwnerFabInsets() { binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt()) } @@ -817,12 +852,18 @@ class CreatorChannelActivity : return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelCommunityFragment } + private fun findFanTalkFragment(): CreatorChannelFanTalkFragment? { + val fragmentTag = "f${CreatorChannelTab.FanTalk.ordinal}" + return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelFanTalkFragment + } + private fun notifyCurrentCreatorChannelTabScrolledToBottom() { when (binding.viewPager.currentItem) { CreatorChannelTab.Live.ordinal -> findLiveFragment()?.onCreatorChannelLiveScrolledToBottom() CreatorChannelTab.Audio.ordinal -> findAudioFragment()?.onCreatorChannelAudioScrolledToBottom() CreatorChannelTab.Series.ordinal -> findSeriesFragment()?.onCreatorChannelSeriesScrolledToBottom() CreatorChannelTab.Community.ordinal -> findCommunityFragment()?.onCreatorChannelCommunityScrolledToBottom() + CreatorChannelTab.FanTalk.ordinal -> findFanTalkFragment()?.onCreatorChannelFanTalkScrolledToBottom() } } @@ -830,7 +871,8 @@ class CreatorChannelActivity : return position == CreatorChannelTab.Live.ordinal || position == CreatorChannelTab.Audio.ordinal || position == CreatorChannelTab.Series.ordinal || - position == CreatorChannelTab.Community.ordinal + position == CreatorChannelTab.Community.ordinal || + position == CreatorChannelTab.FanTalk.ordinal } private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) { @@ -928,6 +970,7 @@ class CreatorChannelActivity : CreatorChannelTab.Live.ordinal -> findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(minHeight) CreatorChannelTab.Audio.ordinal -> findAudioFragment()?.onCreatorChannelAudioViewportHeightChanged(minHeight) CreatorChannelTab.Series.ordinal -> findSeriesFragment()?.onCreatorChannelSeriesViewportHeightChanged(minHeight) + CreatorChannelTab.FanTalk.ordinal -> findFanTalkFragment()?.onCreatorChannelFanTalkViewportHeightChanged(minHeight) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt index 3bdbe976..e18c526c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragment import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFragment @@ -25,6 +26,7 @@ class CreatorChannelPagerAdapter( CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId) CreatorChannelTab.Series -> CreatorChannelSeriesFragment.newInstance(creatorId) CreatorChannelTab.Community -> CreatorChannelCommunityFragment.newInstance(creatorId) + CreatorChannelTab.FanTalk -> CreatorChannelFanTalkFragment.newInstance(creatorId) else -> CreatorChannelPlaceholderFragment.newInstance(tab) } } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt index c39ac193..60cdbd93 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt @@ -416,7 +416,7 @@ class CreatorChannelActivitySourceTest { assertFalse(source.contains("if (tab != CreatorChannelTab.Home) return")) assertTrue(pagerAdapter.contains("CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId)")) assertTrue(pagerAdapter.contains("CreatorChannelTab.Series -> CreatorChannelSeriesFragment.newInstance(creatorId)")) - assertFalse(source.contains("CreatorChannelTab.FanTalk ->")) + assertTrue(pagerAdapter.contains("CreatorChannelTab.FanTalk -> CreatorChannelFanTalkFragment.newInstance(creatorId)")) assertFalse(source.contains("CreatorChannelTab.Donation ->")) } @@ -487,6 +487,7 @@ class CreatorChannelActivitySourceTest { assertFalse(source.contains("onClickDelete = {},")) assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()")) assertTrue( + fragment.contains( "CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }" ) @@ -495,6 +496,47 @@ class CreatorChannelActivitySourceTest { assertTrue(fragment.contains("mediaPlayerManager?.stopContent()")) } + @Test + fun `FanTalk tab source는 Fragment Host pagination height delete dialog를 Activity에 연결한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt" + ).readText() + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt" + ).takeIf { it.exists() }?.readText().orEmpty() + + assertTrue(adapter.contains("CreatorChannelFanTalkFragment.newInstance(creatorId)")) + assertTrue(source.contains("CreatorChannelFanTalkFragment.Host")) + assertTrue( + source.contains( + "import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragment" + ) + ) + assertTrue(source.contains("private fun findFanTalkFragment(): CreatorChannelFanTalkFragment?")) + assertTrue(source.contains("CreatorChannelTab.FanTalk.ordinal -> binding.viewPager.post")) + assertTrue(source.contains("findFanTalkFragment()?.onCreatorChannelFanTalkTabSelected()")) + assertTrue(source.contains("if (binding.viewPager.currentItem == CreatorChannelTab.FanTalk.ordinal)")) + assertTrue(source.contains("findFanTalkFragment()?.onCreatorChannelFanTalkScrolledToBottom()")) + assertTrue(source.contains("findFanTalkFragment()?.onCreatorChannelFanTalkViewportHeightChanged(minHeight)")) + assertTrue(source.contains("position == CreatorChannelTab.FanTalk.ordinal")) + assertTrue(source.contains("override fun onCreatorChannelFanTalkContentChanged()")) + assertTrue(source.contains("override fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long)")) + assertTrue(source.contains("SodaDialog(")) + assertTrue(source.contains("R.string.screen_user_profile_cheer_delete_title")) + assertTrue(source.contains("R.string.screen_user_profile_cheer_delete_desc")) + assertTrue(source.contains("R.string.confirm_delete_title")) + assertTrue(source.contains("R.string.cancel")) + assertTrue(source.contains("findFanTalkFragment()?.onCreatorChannelFanTalkDeleteConfirmed(fanTalkId)")) + assertTrue(source.contains("findFanTalkFragment()?.onCreatorChannelFanTalkRefreshRequested()")) + assertTrue(fragment.contains("interface Host")) + assertTrue(fragment.contains("fun isCreatorChannelOwner(): Boolean")) + assertTrue(fragment.contains("fun onCreatorChannelFanTalkContentChanged()")) + assertTrue(fragment.contains("fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long)")) + } + @Test fun `section adapter source는 활동 지표를 행 단위 resource label로 표시한다`() { val adapter = projectFile( diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt index 83f40199..1c282723 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.fragment.app.FragmentActivity import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragment import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFragment @@ -20,7 +21,7 @@ import org.robolectric.annotation.Config class CreatorChannelPagerAdapterTest { @Test - fun `createFragment는 Home Live Audio Series Community를 실제 Fragment로 생성하고 나머지는 placeholder를 유지한다`() { + fun `createFragment는 Home Live Audio Series Community FanTalk를 실제 Fragment로 생성하고 나머지는 placeholder를 유지한다`() { val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get() val adapter = CreatorChannelPagerAdapter(activity, creatorId = 123L) @@ -29,13 +30,15 @@ class CreatorChannelPagerAdapterTest { assertTrue(adapter.createFragment(CreatorChannelTab.Audio.ordinal) is CreatorChannelAudioFragment) assertTrue(adapter.createFragment(CreatorChannelTab.Series.ordinal) is CreatorChannelSeriesFragment) assertTrue(adapter.createFragment(CreatorChannelTab.Community.ordinal) is CreatorChannelCommunityFragment) + assertTrue(adapter.createFragment(CreatorChannelTab.FanTalk.ordinal) is CreatorChannelFanTalkFragment) CreatorChannelTab.entries .filterNot { it == CreatorChannelTab.Home || it == CreatorChannelTab.Live || it == CreatorChannelTab.Audio || it == CreatorChannelTab.Series || - it == CreatorChannelTab.Community + it == CreatorChannelTab.Community || + it == CreatorChannelTab.FanTalk } .forEach { tab -> assertTrue(adapter.createFragment(tab.ordinal) is CreatorChannelPlaceholderFragment) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt new file mode 100644 index 00000000..f4d5d101 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt @@ -0,0 +1,190 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.fantalk + +import android.app.Application +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import kr.co.vividnext.sodalive.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.io.File + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = Application::class) +class CreatorChannelFanTalkFragmentLayoutTest { + + @Test + fun `팬톡 fragment layout은 count list empty error retry floating write를 제공하고 sort UI는 제외한다`() { + val root = inflateView(R.layout.fragment_creator_channel_fantalk) + val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_fantalk.xml").readText() + val countBar = requireNotNull(root.findViewById(R.id.layout_creator_channel_fantalk_count_bar)) + val fanTalkList = requireNotNull(root.findViewById(R.id.rv_creator_channel_fantalk)) + val emptyContainer = requireNotNull(root.findViewById(R.id.layout_creator_channel_fantalk_empty)) + val emptyMessage = requireNotNull(root.findViewById(R.id.tv_creator_channel_fantalk_empty_message)) + val emptyButton = requireNotNull(root.findViewById(R.id.layout_creator_channel_fantalk_empty_write_button)) + val errorMessage = requireNotNull(root.findViewById(R.id.tv_creator_channel_fantalk_error_message)) + val retryButton = requireNotNull(root.findViewById(R.id.btn_creator_channel_fantalk_retry)) + val floatingWriteButton = requireNotNull(root.findViewById(R.id.btn_creator_channel_fantalk_write)) + + assertSame(root, countBar.parent) + assertSame(root, fanTalkList.parent) + assertSame(root, emptyContainer.parent) + assertSame(emptyContainer, emptyMessage.parent) + assertSame(emptyContainer, emptyButton.parent) + assertSame(root, errorMessage.parent) + assertSame(root, retryButton.parent) + assertSame(root, floatingWriteButton.parent) + assertEquals(false, fanTalkList.clipToPadding) + assertNotNull(countBar.findViewById(R.id.tv_creator_channel_fantalk_total_label)) + assertNotNull(countBar.findViewById(R.id.tv_creator_channel_fantalk_total_count)) + assertFalse(layout.contains("sort")) + assertFalse(layout.contains("정렬")) + assertFalse(layout.contains("iv_creator_channel_fantalk_sort")) + assertTrue(layout.contains("android:text=\"@string/creator_channel_fantalk_all_label\"")) + assertTrue(layout.contains("android:text=\"@string/creator_channel_fantalk_empty_message\"")) + assertTrue(layout.contains("android:gravity=\"center\"")) + assertTrue(layout.contains("android:layout_height=\"0dp\"")) + assertTrue(layout.contains("app:layout_constraintBottom_toBottomOf=\"parent\"")) + assertTrue(layout.contains("android:text=\"@string/creator_channel_fantalk_support_action\"")) + assertTrue(layout.contains("tools:listitem=\"@layout/item_creator_channel_fantalk\"")) + assertTrue(layout.contains("@drawable/ic_new_fantalk_plus")) + assertLocaleString("values/strings.xml", "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!") + assertLocaleString("values-en/strings.xml", "No cheers yet.\\nBe the first to cheer for the creator!") + assertLocaleString("values-ja/strings.xml", "まだ応援がありません。\\n最初にクリエイターを応援してみましょう!") + } + + @Test + fun `팬톡 item layout은 원글 action과 reply container를 제공한다`() { + val item = inflateView(R.layout.item_creator_channel_fantalk) + val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_fantalk.xml").readText() + val replyBackground = projectFile( + "app/src/main/res/drawable/bg_creator_channel_fantalk_reply.xml" + ).readText() + + assertNotNull(item.findViewById(R.id.iv_creator_channel_fantalk_profile)) + assertNotNull(item.findViewById(R.id.tv_creator_channel_fantalk_nickname)) + assertNotNull(item.findViewById(R.id.tv_creator_channel_fantalk_time)) + assertNotNull(item.findViewById(R.id.tv_creator_channel_fantalk_content)) + assertNotNull(item.findViewById(R.id.tv_creator_channel_fantalk_report)) + assertNotNull(item.findViewById(R.id.iv_creator_channel_fantalk_more)) + assertNotNull(item.findViewById(R.id.view_creator_channel_fantalk_reply_connector)) + assertNotNull(item.findViewById(R.id.layout_creator_channel_fantalk_reply)) + assertNotNull(item.findViewById(R.id.iv_creator_channel_fantalk_reply_profile)) + assertTrue(itemLayout.contains("@drawable/ic_new_more")) + assertTrue(itemLayout.contains("android:text=\"@string/creator_channel_fantalk_report\"")) + assertTrue(itemLayout.contains("android:layout_width=\"42dp\"")) + assertTrue(itemLayout.contains("android:layout_height=\"42dp\"")) + assertTrue(itemLayout.contains("android:layout_width=\"20dp\"")) + assertTrue(itemLayout.contains("android:layout_height=\"20dp\"")) + assertTrue(itemLayout.contains("@+id/barrier_creator_channel_fantalk_action_start")) + assertTrue(itemLayout.contains("app:barrierDirection=\"start\"")) + assertTrue( + itemLayout.contains( + "app:constraint_referenced_ids=\"tv_creator_channel_fantalk_report,iv_creator_channel_fantalk_more\"" + ) + ) + assertTrue( + itemLayout.contains( + "app:layout_constraintEnd_toStartOf=\"@id/barrier_creator_channel_fantalk_action_start\"" + ) + ) + assertTrue(itemLayout.contains("@drawable/bg_creator_channel_fantalk_reply")) + assertTrue(replyBackground.contains("@color/gray_900")) + } + + @Test + fun `팬톡 more popup layout은 수정 삭제 TextView를 제공한다`() { + val popup = inflateView(R.layout.view_creator_channel_fantalk_more_popup) + val popupLayout = projectFile("app/src/main/res/layout/view_creator_channel_fantalk_more_popup.xml").readText() + + assertNotNull(popup.findViewById(R.id.tv_creator_channel_fantalk_more_edit)) + assertNotNull(popup.findViewById(R.id.tv_creator_channel_fantalk_more_delete)) + assertTrue(popupLayout.contains("android:text=\"@string/creator_channel_fantalk_edit\"")) + assertTrue(popupLayout.contains("android:text=\"@string/creator_channel_fantalk_delete\"")) + assertTrue(popupLayout.contains("@drawable/bg_creator_channel_sort_popup")) + } + + @Test + fun `팬톡 adapter와 popup source는 Phase 4 바인딩 계약을 포함한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt" + ).readText() + val popup = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkMorePopup.kt" + ).readText() + + assertTrue(adapter.contains("ItemCreatorChannelFantalkBinding")) + assertTrue(adapter.contains("fun submitItems(items: List)")) + assertTrue(adapter.contains("notifyDataSetChanged()")) + assertTrue(adapter.contains("CircleCropTransformation()")) + assertTrue(adapter.contains("R.drawable.ic_placeholder_profile")) + assertTrue(adapter.contains("CreatorChannelFanTalkRightAction.Report")) + assertTrue(adapter.contains("CreatorChannelFanTalkRightAction.OwnerMore")) + assertFalse(adapter.contains("root.setOnClickListener")) + assertTrue(popup.contains("PopupWindow(anchor.context)")) + assertTrue(popup.contains("tvCreatorChannelFantalkMoreEdit.isVisible = showEdit")) + assertTrue(popup.contains("tvCreatorChannelFantalkMoreDelete.isVisible = showDelete")) + assertTrue(popup.contains("onDeleteClick(fanTalkId)")) + } + + @Test + fun `팬톡 fragment source는 Phase 5 상태 신고 삭제 Host 계약을 포함한다`() { + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt" + ).readText() + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt" + ).readText() + + assertTrue(fragment.contains("BaseFragment")) + assertTrue(fragment.contains("private val viewModel: CreatorChannelFanTalkViewModel by viewModel()")) + assertTrue(fragment.contains("layoutManager = LinearLayoutManager(requireContext())")) + assertTrue(fragment.contains("onCreatorChannelFanTalkTabSelected()")) + assertTrue(fragment.contains("viewModel.loadFanTalks(creatorId, isOwner = host.isCreatorChannelOwner())")) + assertTrue(fragment.contains("onCreatorChannelFanTalkScrolledToBottom()")) + assertTrue(fragment.contains("viewModel.loadMore()")) + assertTrue(fragment.contains("onCreatorChannelFanTalkRefreshRequested()")) + assertTrue(fragment.contains("viewModel.refreshFanTalks()")) + assertTrue(fragment.contains("onCreatorChannelFanTalkViewportHeightChanged(minHeight: Int)")) + assertTrue(fragment.contains("binding.root.minimumHeight = minHeight")) + assertTrue(fragment.contains("onCreatorChannelFanTalkDeleteConfirmed(fanTalkId: Long)")) + assertTrue(fragment.contains("viewModel.deleteFanTalk(fanTalkId)")) + assertTrue(fragment.contains("CheersReportDialog(requireActivity(), layoutInflater)")) + assertTrue(fragment.contains("R.string.screen_user_profile_fantalk_report_reason_required")) + assertTrue(fragment.contains("viewModel.reportFanTalk(item.fanTalkId, reason)")) + assertTrue(fragment.contains("CreatorChannelFanTalkMorePopup(")) + assertTrue(fragment.contains("onDeleteClick = host::onCreatorChannelFanTalkDeleteClicked")) + assertTrue(fragment.contains("fun onCreatorChannelFanTalkContentChanged()")) + assertTrue(fragment.contains("fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long)")) + assertTrue(adapter.contains("private val onReportClick: (CreatorChannelFanTalkUiModel) -> Unit")) + assertTrue(adapter.contains("tvCreatorChannelFantalkReport.setOnClickListener { onReportClick(item) }")) + } + + private fun inflateView(layoutResId: Int): View { + val context = ApplicationProvider.getApplicationContext() + return LayoutInflater.from(context).inflate(layoutResId, null, false) + } + + private fun projectFile(path: String): File = File(projectRoot(), path) + + private fun assertLocaleString(path: String, expectedValue: String) { + val strings = projectFile("app/src/main/res/$path").readText() + assertTrue(strings.contains("$expectedValue")) + } + + private fun projectRoot(): File { + return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile } + .first { File(it, "settings.gradle").exists() } + } +}