test(creator): 채널 홈 화면 계약을 검증한다
This commit is contained in:
@@ -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<ActivityCreatorChannelHomeBinding>"))
|
||||
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("<HorizontalScrollView"))
|
||||
assertTrue(layout.contains("@+id/horizontal_tab_scroll_view"))
|
||||
assertTrue(layout.contains("@+id/tab_container"))
|
||||
assertTrue(layout.contains("@+id/rv_home_sections"))
|
||||
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_talk\""))
|
||||
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_dm\""))
|
||||
assertFalse(layout.contains("TextTabBarView"))
|
||||
assertTrue(source.contains("getString(tab.labelResId)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `title bar source는 Figma 상태별 capsule 구조를 사용한다`() {
|
||||
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("@+id/layout_follow_capsule"))
|
||||
assertTrue(layout.contains("@+id/tv_follow_label"))
|
||||
assertTrue(layout.contains("@drawable/bg_creator_channel_follow_capsule"))
|
||||
assertTrue(layout.contains("@drawable/bg_creator_channel_following_capsule"))
|
||||
assertTrue(layout.contains("android:paddingHorizontal=\"@dimen/spacing_8\""))
|
||||
assertTrue(source.contains("binding.layoutFollowCapsule"))
|
||||
assertTrue(source.contains("binding.tvFollowLabel.isVisible = !header.isFollow"))
|
||||
assertFalse(layout.contains("@+id/tv_follow\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creator channel home은 status bar 뒤까지 header를 그리고 자체 inset을 처리한다`() {
|
||||
val baseActivity = projectFile("app/src/main/java/kr/co/vividnext/sodalive/base/BaseActivity.kt").readText()
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(baseActivity.contains("shouldApplySystemBarTopInset"))
|
||||
assertTrue(baseActivity.contains("if (shouldApplySystemBarTopInset) systemBars.top else 0"))
|
||||
assertTrue(source.contains("override val shouldApplySystemBarTopInset: Boolean = false"))
|
||||
assertTrue(source.contains("setTitleBarTopInset"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creator channel home은 어두운 header 위 status bar icon을 밝게 표시한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("WindowCompat.getInsetsController(window, binding.root)"))
|
||||
assertTrue(source.contains("isAppearanceLightStatusBars = false"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tab source는 Figma 기준 selected indicator와 16sp 고정 폭 탭을 사용한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeActivity.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("createTabView(tab, isSelected = index == 0)"))
|
||||
assertTrue(source.contains("tabText.textSize = 16f"))
|
||||
assertTrue(source.contains("width = 110.dpToPx().toInt()"))
|
||||
assertTrue(source.contains("indicator.setBackgroundColor(getColor(R.color.soda_400))"))
|
||||
assertTrue(source.contains("indicator.isVisible = isSelected"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `section adapter source는 활동 지표를 행 단위 resource label로 표시한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(adapter.contains("creator_channel_activity_debut"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_debut_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_count_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_duration_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_contributor_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_audio_count_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_series_count_format"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_count"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_duration"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_live_contributor"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_audio_count"))
|
||||
assertTrue(adapter.contains("creator_channel_activity_series_count"))
|
||||
assertFalse(adapter.contains("addActivityRow(activity.dDay, activity.debutDateUtc.orEmpty())"))
|
||||
assertFalse(adapter.contains("\"Live "))
|
||||
assertFalse(adapter.contains("\"Audio "))
|
||||
assertFalse(adapter.contains("\"Series "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `section adapter source는 Figma 섹션별 데이터를 공통 카드 하나로 축약하지 않는다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(adapter.contains("bindAudioContents"))
|
||||
assertTrue(adapter.contains("bindSeries"))
|
||||
assertTrue(adapter.contains("bindCommunities"))
|
||||
assertTrue(adapter.contains("bindFanTalk"))
|
||||
assertTrue(adapter.contains("bindSns"))
|
||||
assertTrue(adapter.contains("bindActivity"))
|
||||
assertTrue(adapter.contains("ll_section_items"))
|
||||
assertTrue(adapter.contains("sectionItems?.addView"))
|
||||
assertTrue(adapter.contains("addActivityRow"))
|
||||
assertTrue(adapter.contains("createContentTile"))
|
||||
assertTrue(adapter.contains("addFeedCard"))
|
||||
assertFalse(adapter.contains("addScheduleRow"))
|
||||
assertFalse(adapter.contains("addDonationCard"))
|
||||
assertTrue(adapter.contains("addCommentCard"))
|
||||
assertTrue(adapter.contains("createSnsButton"))
|
||||
assertTrue(adapter.contains("activity.debutDateUtc"))
|
||||
assertTrue(adapter.contains("activity.liveDurationHours"))
|
||||
assertTrue(adapter.contains("activity.liveContributorCount"))
|
||||
assertFalse(adapter.contains("audioContents.joinToString { it.title }"))
|
||||
assertFalse(adapter.contains("series.joinToString { it.title }"))
|
||||
assertFalse(adapter.contains("communities.firstOrNull()?.content"))
|
||||
assertFalse(adapter.contains("joinToString(separator = \"\\n\")"))
|
||||
assertFalse(adapter.contains("private fun addCard"))
|
||||
assertFalse(adapter.contains("getChildAt"))
|
||||
assertFalse(adapter.contains("keepLegacyViewsReferenced"))
|
||||
assertFalse(adapter.contains("fun CreatorChannelHomeSection.imageUrl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `현재 라이브 섹션은 전용 layout과 bind로 렌더링한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||
).readText()
|
||||
val liveLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_live.xml").readText()
|
||||
|
||||
assertTrue(liveLayout.contains("@+id/layout_current_live_card"))
|
||||
assertTrue(liveLayout.contains("@+id/tv_current_live_title"))
|
||||
assertTrue(liveLayout.contains("@+id/tv_current_live_start_time"))
|
||||
assertTrue(liveLayout.contains("@+id/tv_current_live_price"))
|
||||
assertTrue(liveLayout.contains("@+id/tv_current_live_adult"))
|
||||
assertTrue(liveLayout.contains("@drawable/bg_creator_channel_current_live"))
|
||||
assertTrue(liveLayout.contains("@drawable/bg_creator_channel_current_live_price"))
|
||||
assertTrue(liveLayout.contains("@drawable/ic_can"))
|
||||
assertTrue(liveLayout.contains("android:layout_height=\"78dp\""))
|
||||
assertFalse(liveLayout.contains("@layout/view_section_title"))
|
||||
assertFalse(liveLayout.contains("@+id/tv_section_title"))
|
||||
assertFalse(liveLayout.contains("@+id/ll_section_items"))
|
||||
assertTrue(adapter.contains("private val currentLiveTitle: TextView?"))
|
||||
assertTrue(adapter.contains("private val currentLiveStartTime: TextView?"))
|
||||
assertTrue(adapter.contains("private val currentLivePrice: TextView?"))
|
||||
assertTrue(adapter.contains("private val currentLiveAdult: TextView?"))
|
||||
assertTrue(adapter.contains("currentLiveTitle?.text = item.live.title"))
|
||||
assertTrue(adapter.contains("currentLivePriceLayout?.isVisible = item.live.price > 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<ImageView>(R.id.iv_notice_profile).loadUrl"))
|
||||
assertTrue(adapter.contains("row.findViewById<TextView>(R.id.tv_notice_creator_name).text = notice.creatorNickname"))
|
||||
assertTrue(adapter.contains("row.findViewById<TextView>(R.id.tv_notice_created_at).text = notice.dateUtc"))
|
||||
assertTrue(adapter.contains("row.findViewById<TextView>(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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user