diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivitySourceTest.kt new file mode 100644 index 00000000..839afee7 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivitySourceTest.kt @@ -0,0 +1,596 @@ +package kr.co.vividnext.sodalive.v2.creator.channel + +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelDonationCardWidthDp +import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelDonationHeaderColorRes +import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelNoticeCardWidthDp +import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount +import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate +import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek +import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleTime +import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSnsButtonSizeDp +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.util.Locale +import java.util.TimeZone + +class CreatorChannelHomeActivitySourceTest { + + @Test + fun `Activity source는 intent helper invalid id ViewModel observe navigation 계약을 연결한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt" + ).readText() + + assertTrue(source.contains("BaseActivity")) + assertTrue(source.contains("const val EXTRA_CREATOR_ID")) + assertTrue(source.contains("fun newIntent(context: Context, creatorId: Long): Intent")) + assertTrue(source.contains("Intent(context, CreatorChannelHomeActivity::class.java)")) + assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)")) + assertTrue(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()")) + assertTrue(source.contains("if (creatorId <= 0L)")) + assertTrue(source.contains("finish()")) + assertTrue(source.contains("viewModel.loadHome(creatorId)")) + assertTrue(source.contains("viewModel.homeStateLiveData.observe(this)")) + assertTrue(source.contains("viewModel.chatRoomIdLiveData.observe(this)")) + assertFalse(source.contains("is CreatorChannelHomeUiState.Error -> showToast")) + assertTrue(source.contains("event.consume()?.let")) + assertTrue(source.contains("ChatRoomActivity.newIntent(this, chatRoomId)")) + assertTrue(source.contains("DmChatRoomActivity.newIntentByCreatorId(this, creatorId)")) + assertTrue(source.contains("viewModel.createChatRoom(characterId)")) + assertTrue(source.contains("updateActionButtonLayout")) + assertTrue(source.contains("marginStart = if (isChatVisible && isDmVisible)")) + assertFalse(source.contains("CreatorFollowNotifyFragment")) + } + + @Test + fun `layout source는 HorizontalScrollView 기반 7개 탭 컨테이너와 RecyclerView를 가진다`() { + val layout = projectFile("app/src/main/res/layout/activity_creator_channel_home.xml").readText() + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt" + ).readText() + + assertTrue(layout.contains(" 0")) + assertTrue(adapter.contains("currentLiveAdult?.isVisible = item.live.isAdult")) + assertFalse(adapter.contains("private fun addHeroCard")) + assertFalse(adapter.contains("addHeroCard(")) + } + + @Test + fun `최신 오디오 섹션은 Figma Contents 변형 전용 layout과 bind로 렌더링한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val latestAudioLayout = projectFile( + "app/src/main/res/layout/item_creator_channel_home_latest_audio.xml" + ).readText() + val thumbnailView = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelLatestAudioThumbnailView.kt" + ).readText() + + assertTrue(latestAudioLayout.contains("@+id/layout_latest_audio_card")) + assertTrue(latestAudioLayout.contains("CreatorChannelLatestAudioThumbnailView")) + assertTrue(latestAudioLayout.contains("@+id/iv_latest_audio_thumbnail")) + assertTrue(latestAudioLayout.contains("@+id/iv_latest_audio_point_tag")) + assertTrue(latestAudioLayout.contains("@+id/tv_latest_audio_new_label")) + assertTrue(latestAudioLayout.contains("@+id/tv_latest_audio_title")) + assertTrue(latestAudioLayout.contains("@+id/tv_latest_audio_duration")) + assertTrue(latestAudioLayout.contains("@drawable/ic_content_tag_point")) + assertTrue(latestAudioLayout.contains("android:fontFamily=\"@font/pattaya_regular\"")) + assertTrue(latestAudioLayout.contains("android:layout_width=\"88dp\"")) + assertTrue(latestAudioLayout.contains("android:layout_height=\"88dp\"")) + assertFalse(latestAudioLayout.contains("@layout/view_section_title")) + assertFalse(latestAudioLayout.contains("@+id/tv_section_title")) + assertFalse(latestAudioLayout.contains("@+id/ll_section_items")) + assertFalse(latestAudioLayout.contains("AudioContentCardView")) + assertFalse(latestAudioLayout.contains("android:clipToOutline")) + assertTrue(thumbnailView.contains("clipToOutline = true")) + assertTrue(thumbnailView.contains("ViewOutlineProvider")) + assertTrue(thumbnailView.contains("outline.setRoundRect")) + assertTrue(thumbnailView.contains("R.dimen.radius_14")) + assertTrue(adapter.contains("private val latestAudioThumbnail: ImageView?")) + assertTrue(adapter.contains("private val latestAudioPointTag: ImageView?")) + assertTrue(adapter.contains("private val latestAudioTitle: TextView?")) + assertTrue(adapter.contains("private val latestAudioDuration: TextView?")) + assertTrue(adapter.contains("latestAudioTitle?.text = item.audioContent.title")) + assertTrue(adapter.contains("latestAudioThumbnail?.loadUrl")) + assertTrue(adapter.contains("latestAudioPointTag?.isVisible = item.audioContent.isPointAvailable")) + assertFalse(adapter.contains("private fun addAudioCard")) + assertFalse(adapter.contains("addAudioCard(item.audioContent)")) + } + + @Test + fun `후원 섹션은 Figma 전용 layout과 row bind로 렌더링한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val donationLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_donation.xml").readText() + val donationRowLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_donation_row.xml").readText() + + assertTrue(donationLayout.contains("@layout/view_section_title")) + assertTrue(donationLayout.contains("@+id/hsv_donation_items")) + assertTrue(donationLayout.contains("@+id/ll_donation_items")) + assertTrue(donationLayout.contains("@+id/layout_donation_button")) + assertTrue(donationLayout.contains("@drawable/ic_new_donation")) + assertFalse(donationLayout.contains("@+id/tv_donation_button")) + assertFalse(donationLayout.contains("@+id/ll_section_items")) + val donationCardView = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelDonationCardView.kt" + ).readText() + + assertTrue(donationRowLayout.contains("CreatorChannelDonationCardView")) + assertTrue(donationRowLayout.contains("@+id/layout_donation_card")) + assertTrue(donationRowLayout.contains("@+id/layout_donation_header")) + assertTrue(donationRowLayout.contains("@+id/iv_donation_profile")) + assertTrue(donationRowLayout.contains("@+id/tv_donation_nickname")) + assertTrue(donationRowLayout.contains("@+id/tv_donation_created_at")) + assertTrue(donationRowLayout.contains("@+id/tv_donation_can")) + assertTrue(donationRowLayout.contains("@+id/tv_donation_message")) + assertTrue(donationRowLayout.contains("@drawable/ic_can")) + assertFalse(donationRowLayout.contains("android:layout_width=\"374dp\"")) + assertTrue(adapter.contains("private val donationItems: LinearLayout?")) + assertTrue(adapter.contains("R.layout.item_creator_channel_home_donation_row")) + assertTrue(adapter.contains("donationItems?.addView(row)")) + assertTrue(adapter.contains("val visibleDonations = item.donations.take(MAX_DONATION_ITEM_COUNT)")) + assertTrue(adapter.contains("calculateCreatorChannelDonationCardWidthDp")) + assertTrue(adapter.contains("calculateCreatorChannelDonationHeaderColorRes(donation.can)")) + assertTrue(adapter.contains("R.string.creator_channel_donation_fallback_message")) + assertFalse(adapter.contains("private fun addDonationCard")) + assertFalse(adapter.contains("addDonationCard(")) + } + + @Test + fun `후원 can 수량은 요청된 배경색 resource 경계로 매핑한다`() { + assertEquals(R.color.gray_200, calculateCreatorChannelDonationHeaderColorRes(1)) + assertEquals(R.color.gray_200, calculateCreatorChannelDonationHeaderColorRes(50)) + assertEquals(R.color.green_400, calculateCreatorChannelDonationHeaderColorRes(51)) + assertEquals(R.color.green_400, calculateCreatorChannelDonationHeaderColorRes(100)) + assertEquals(R.color.creator_channel_donation_cyan, calculateCreatorChannelDonationHeaderColorRes(101)) + assertEquals(R.color.creator_channel_donation_cyan, calculateCreatorChannelDonationHeaderColorRes(499)) + assertEquals(R.color.red_400, calculateCreatorChannelDonationHeaderColorRes(500)) + } + + @Test + fun `후원 컴포넌트 width는 402dp 기준 최대 374dp이고 작은 화면에서는 비율 축소한다`() { + assertEquals(374, calculateCreatorChannelDonationCardWidthDp(402)) + assertEquals(374, calculateCreatorChannelDonationCardWidthDp(430)) + assertEquals(335, calculateCreatorChannelDonationCardWidthDp(360)) + } + + @Test + fun `후원 빈 메시지 fallback은 다국어 string resource로 제공한다`() { + val ko = projectFile("app/src/main/res/values/strings.xml").readText() + val en = projectFile("app/src/main/res/values-en/strings.xml").readText() + val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText() + + assertTrue(ko.contains("creator_channel_donation_fallback_message")) + assertTrue(ko.contains("%1\$d캔을 후원하였습니다.")) + assertTrue(en.contains("creator_channel_donation_fallback_message")) + assertTrue(ja.contains("creator_channel_donation_fallback_message")) + } + + @Test + fun `공지 섹션은 최대 3개 Figma feed card를 가로 row로 렌더링한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val noticeLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_notice.xml").readText() + val noticeRowLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_notice_row.xml").readText() + val noticeCardView = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelNoticeCardView.kt" + ).readText() + val noticeThumbnailView = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelNoticeThumbnailView.kt" + ).readText() + + assertTrue(noticeLayout.contains("@layout/view_section_title")) + assertTrue(noticeLayout.contains("@+id/hsv_notice_items")) + assertTrue(noticeLayout.contains("@+id/ll_notice_items")) + assertFalse(noticeLayout.contains("@+id/ll_section_items")) + assertTrue(noticeRowLayout.contains("CreatorChannelNoticeCardView")) + assertTrue(noticeRowLayout.contains("@+id/layout_notice_card")) + assertTrue(noticeRowLayout.contains("@drawable/ic_pin")) + assertTrue(noticeRowLayout.contains("@+id/tv_notice_label")) + assertTrue(noticeRowLayout.contains("@+id/iv_notice_profile")) + assertTrue(noticeRowLayout.contains("@+id/tv_notice_creator_name")) + assertTrue(noticeRowLayout.contains("@+id/tv_notice_created_at")) + assertTrue(noticeRowLayout.contains("@+id/tv_notice_content")) + assertTrue(noticeRowLayout.contains("CreatorChannelNoticeThumbnailView")) + assertTrue(noticeRowLayout.contains("@+id/iv_notice_thumbnail")) + assertFalse(noticeRowLayout.contains("android:clipToOutline")) + assertTrue(noticeCardView.contains("clipToOutline = true")) + assertTrue(noticeCardView.contains("ViewOutlineProvider")) + assertTrue(noticeCardView.contains("outline.setRoundRect")) + assertTrue(noticeCardView.contains("R.dimen.radius_14")) + assertTrue(noticeThumbnailView.contains("clipToOutline = true")) + assertTrue(noticeThumbnailView.contains("ViewOutlineProvider")) + assertTrue(noticeThumbnailView.contains("outline.setRoundRect")) + assertTrue(noticeThumbnailView.contains("R.dimen.radius_14")) + assertTrue(adapter.contains("private val noticeItems: LinearLayout?")) + assertTrue(adapter.contains("val visibleNotices = item.notices.take(MAX_NOTICE_ITEM_COUNT)")) + assertTrue(adapter.contains("R.layout.item_creator_channel_home_notice_row")) + assertTrue(adapter.contains("noticeItems?.addView(row)")) + assertTrue(adapter.contains("row.findViewById(R.id.iv_notice_profile).loadUrl")) + assertTrue(adapter.contains("row.findViewById(R.id.tv_notice_creator_name).text = notice.creatorNickname")) + assertTrue(adapter.contains("row.findViewById(R.id.tv_notice_created_at).text = notice.dateUtc")) + assertTrue(adapter.contains("row.findViewById(R.id.tv_notice_content).text = notice.content")) + assertTrue(adapter.contains("noticeThumbnail.isVisible = !notice.imageUrl.isNullOrBlank()")) + assertTrue(adapter.contains("marginEnd = if (index == visibleNotices.lastIndex) 0 else 4.dp()")) + assertTrue(adapter.contains("private const val MAX_NOTICE_ITEM_COUNT = 3")) + val legacyNoticeBinding = """ + private fun bindNotices(item: CreatorChannelHomeSection.Notices) { + item.notices.forEach { notice -> + addTextCard( + """.trimIndent() + assertFalse(adapter.contains(legacyNoticeBinding)) + } + + @Test + fun `공지 컴포넌트 width는 402dp 기준 최대 346dp이고 작은 화면에서는 비율 축소한다`() { + assertEquals(346, calculateCreatorChannelNoticeCardWidthDp(402)) + assertEquals(346, calculateCreatorChannelNoticeCardWidthDp(430)) + assertEquals(310, calculateCreatorChannelNoticeCardWidthDp(360)) + } + + @Test + fun `section adapter source는 가로 시리즈와 SNS 링크와 일정 타입 label을 보존한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertTrue(adapter.contains("HorizontalScrollView")) + assertTrue(adapter.contains("createHorizontalScrollRow")) + assertTrue(adapter.contains("createHorizontalScrollRow(row)")) + assertTrue(adapter.contains("iconResId = sns.iconResId")) + assertTrue(adapter.contains("url = sns.url")) + assertTrue(adapter.contains("schedule.type.labelResId")) + assertFalse(adapter.contains("schedule.type.code")) + } + + @Test + fun `일정 섹션은 Figma 전용 layout과 row bind로 최대 3개를 렌더링한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val scheduleLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_schedule.xml").readText() + val scheduleRowLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_schedule_row.xml").readText() + + assertTrue(scheduleLayout.contains("@layout/view_section_title")) + assertTrue(scheduleLayout.contains("@+id/ll_schedule_timeline")) + assertTrue(scheduleLayout.contains("@+id/ll_schedule_items")) + assertFalse(scheduleLayout.contains("@+id/ll_section_items")) + assertTrue(scheduleRowLayout.contains("@+id/layout_schedule_row")) + assertTrue(scheduleRowLayout.contains("@+id/tv_schedule_date")) + assertTrue(scheduleRowLayout.contains("@+id/tv_schedule_day_of_week")) + assertTrue(scheduleRowLayout.contains("@+id/tv_schedule_title")) + assertTrue(scheduleRowLayout.contains("@+id/tv_schedule_type")) + assertTrue(scheduleRowLayout.contains("@+id/tv_schedule_time")) + assertTrue(adapter.contains("private val scheduleItems: LinearLayout?")) + assertTrue(adapter.contains("val visibleSchedules = item.schedules.take(MAX_SCHEDULE_ITEM_COUNT)")) + assertTrue(adapter.contains("R.layout.item_creator_channel_home_schedule_row")) + assertTrue(adapter.contains("scheduleItems?.addView(row)")) + assertTrue(adapter.contains("formatCreatorChannelScheduleDate(schedule.scheduledAtUtc)")) + assertTrue(adapter.contains("formatCreatorChannelScheduleDayOfWeek(schedule.scheduledAtUtc)")) + assertTrue(adapter.contains("formatCreatorChannelScheduleTime(schedule.scheduledAtUtc)")) + assertTrue(adapter.contains("row.setOnClickListener { onScheduleClick(schedule) }")) + assertTrue(adapter.contains("private const val MAX_SCHEDULE_ITEM_COUNT = 3")) + assertFalse(adapter.contains("private fun addScheduleRow")) + assertFalse(adapter.contains("addScheduleRow(")) + } + + @Test + fun `일정 timeline indicator는 표시 스케줄 개수에 맞춰 점과 line을 동적으로 만든다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertEquals(0, calculateCreatorChannelScheduleTimelineLineCount(0)) + assertEquals(0, calculateCreatorChannelScheduleTimelineLineCount(1)) + assertEquals(1, calculateCreatorChannelScheduleTimelineLineCount(2)) + assertEquals(2, calculateCreatorChannelScheduleTimelineLineCount(3)) + assertTrue(adapter.contains("private val scheduleTimeline: LinearLayout?")) + assertTrue(adapter.contains("scheduleTimeline?.removeAllViews()")) + assertTrue(adapter.contains("bindScheduleTimeline(visibleSchedules.size)")) + assertTrue(adapter.contains("repeat(count) { index ->")) + assertTrue(adapter.contains("R.drawable.bg_creator_channel_schedule_timeline_dot")) + assertTrue(adapter.contains("R.drawable.bg_creator_channel_schedule_timeline_line")) + assertTrue(adapter.contains("if (index < calculateCreatorChannelScheduleTimelineLineCount(count))")) + } + + @Test + fun `일정 UTC 시간은 디바이스 timezone 기준 날짜 요일 시간으로 표시한다`() { + val timeZone = TimeZone.getTimeZone("Asia/Seoul") + val locale = Locale.KOREA + + assertEquals("30", formatCreatorChannelScheduleDate("2026-06-29T15:00:00Z", timeZone, locale)) + assertEquals("화", formatCreatorChannelScheduleDayOfWeek("2026-06-29T15:00:00Z", timeZone, locale)) + assertEquals("오전 12:00", formatCreatorChannelScheduleTime("2026-06-29T15:00:00Z", timeZone, locale)) + } + + @Test + fun `일정 클릭은 콘텐츠 상세와 라이브 상세 이동 계약을 연결한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt" + ).readText() + + assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked)")) + assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)")) + assertTrue(source.contains("CreatorActivityType.Audio")) + assertTrue(source.contains("CreatorActivityType.LiveReplay")) + assertTrue(source.contains("AudioContentDetailActivity::class.java")) + assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)")) + assertTrue(source.contains("CreatorActivityType.Live")) + assertTrue(source.contains("CreatorActivityType.Live -> showLiveRoomDetail(schedule.targetId)")) + assertTrue(source.contains("LiveRoomDetailFragment(")) + assertTrue(source.contains("detailFragment.show(supportFragmentManager, detailFragment.tag)")) + } + + @Test + fun `section adapter source는 오디오 콘텐츠를 단일 가로 스크롤 row로 표시한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertTrue(adapter.contains("private fun bindAudioContents")) + assertTrue(adapter.contains("item.audioContents.forEach { audioContent ->")) + assertTrue(adapter.contains("row.addView(createAudioTile(audioContent))")) + assertTrue(adapter.contains("sectionItems?.addView(createHorizontalScrollRow(row))")) + assertFalse(adapter.contains("item.audioContents.forEach { audioContent ->\n addAudioCard(audioContent)")) + } + + @Test + fun `SNS source는 ic_sns 아이콘을 ImageView로 표시한다`() { + 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() + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertTrue(uiModel.contains("val iconResId: Int")) + assertTrue(mapper.contains("R.drawable.ic_sns_instagram")) + assertTrue(mapper.contains("R.drawable.ic_sns_youtube")) + assertTrue(mapper.contains("R.drawable.ic_sns_x")) + assertTrue(mapper.contains("R.drawable.ic_sns_kakao")) + assertTrue(mapper.contains("R.drawable.ic_sns_fancimm")) + assertTrue(adapter.contains("private fun createSnsButton(iconResId: Int, url: String, isLast: Boolean): ImageView")) + assertTrue(adapter.contains("setImageResource(iconResId)")) + assertFalse(adapter.contains("createText(label, R.style.Typography_Caption2, R.color.white")) + } + + @Test + fun `SNS source는 유효 URL만 매핑하고 열 수 있는 intent만 실행한다`() { + val mapper = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt" + ).readText() + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertTrue(mapper.contains("isValidCreatorChannelSnsUrl")) + assertTrue(adapter.contains("resolveActivity(itemView.context.packageManager)")) + assertTrue(adapter.contains("itemView.context.startActivity(intent)")) + assertFalse(adapter.contains("itemView.context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))")) + } + + @Test + fun `SNS 버튼 크기는 402dp 기준 52dp를 최대값으로 하고 작은 화면에서는 비례 축소한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertEquals(52, calculateCreatorChannelSnsButtonSizeDp(402)) + assertEquals(52, calculateCreatorChannelSnsButtonSizeDp(430)) + assertEquals(47, calculateCreatorChannelSnsButtonSizeDp(360)) + assertTrue(adapter.contains("calculateCreatorChannelSnsButtonSizeDp")) + assertFalse(adapter.contains("LinearLayout.LayoutParams(52.dp(), 52.dp())")) + } + + @Test + fun `SNS 버튼 row는 작은 화면 비율 축소를 유지하고 마지막 아이콘 trailing margin은 제거한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + + assertTrue(adapter.contains("item.items.forEachIndexed { index, sns ->")) + assertTrue(adapter.contains("isLast = index == item.items.lastIndex")) + assertTrue(adapter.contains("private fun createSnsButton(iconResId: Int, url: String, isLast: Boolean): ImageView")) + assertTrue(adapter.contains("marginEnd = if (isLast) 0 else 16.dp()")) + } + + @Test + fun `section item layouts는 legacy generic card id를 제거하고 동적 컨테이너만 둔다`() { + val layoutNames = listOf( + "audio", + "series", + "community", + "fantalk", + "introduce", + "activity", + "sns" + ) + + layoutNames.forEach { name -> + val layout = projectFile("app/src/main/res/layout/item_creator_channel_home_$name.xml").readText() + assertTrue(layout.contains("@+id/ll_section_items")) + assertFalse(layout.contains("@+id/iv_thumbnail")) + assertFalse(layout.contains("@+id/tv_primary")) + assertFalse(layout.contains("@+id/tv_secondary")) + } + } + + @Test + fun `Manifest source는 CreatorChannelHomeActivity를 등록한다`() { + val manifest = projectFile("app/src/main/AndroidManifest.xml").readText() + + assertTrue(manifest.contains(".v2.creator.channel.CreatorChannelHomeActivity")) + } + + @Test + fun `채팅과 DM Activity intent helper 계약을 참조한다`() { + val chatRoom = projectFile("app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt").readText() + val dmRoom = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt").readText() + + assertTrue(chatRoom.contains("fun newIntent(context: Context, roomId: Long): Intent")) + assertTrue(chatRoom.contains("putExtra(EXTRA_ROOM_ID, roomId)")) + assertTrue(dmRoom.contains("fun newIntentByCreatorId(context: Context, creatorId: Long): Intent")) + assertTrue(dmRoom.contains("putExtra(EXTRA_CREATOR_ID, creatorId)")) + } + + private fun projectFile(relativePath: String): File { + val candidates = listOf(File(relativePath), File("../$relativePath")) + return candidates.firstOrNull { it.exists() } + ?: error("Project file not found: $relativePath") + } +}