diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt index 597da810..4ca5b085 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt @@ -2,24 +2,32 @@ package kr.co.vividnext.sodalive.v2.main.home import android.app.Application import android.content.Context +import android.content.res.Configuration import android.graphics.drawable.ColorDrawable import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.GridLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.widget.NestedScrollView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel +import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel +import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorGroupUiModel +import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel +import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowAllButtonBinder +import kr.co.vividnext.sodalive.v2.main.home.ui.HomeGenreCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter import kr.co.vividnext.sodalive.v2.widget.AudioContentCardView @@ -35,6 +43,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config +import java.util.Locale @RunWith(RobolectricTestRunner::class) @Config(sdk = [28], application = Application::class) @@ -263,6 +272,219 @@ class HomeMainFragmentLayoutTest { assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, viewHolder.itemView.layoutParams.height) } + @Test + fun `genre creator group item matches figma snapping card structure`() { + val group = inflateViewWithParent(R.layout.item_home_genre_creator_group) + val titleGenre = group.findViewById(R.id.tv_home_genre_creator_group_title_genre) + val titleSuffix = group.findViewById(R.id.tv_home_genre_creator_group_title_suffix) + val creatorGrid = group.findViewById(R.id.gl_home_genre_creator_profiles) + val followAllButton = group.findViewById(R.id.view_home_genre_group_follow_all) + + assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, group.layoutParams.width) + assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, group.layoutParams.height) + assertEquals(R.drawable.bg_home_genre_creator_group, shadowOf(group.background).createdFromResId) + assertEquals(14.dpToPx(), group.paddingStart) + assertEquals(14.dpToPx(), group.paddingTop) + assertEquals(14.dpToPx(), group.paddingEnd) + assertEquals(14.dpToPx(), group.paddingBottom) + assertNotNull(titleGenre) + assertNotNull(titleSuffix) + assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, creatorGrid.layoutParams.width) + assertEquals(4, creatorGrid.columnCount) + assertEquals(2, creatorGrid.rowCount) + assertNotNull(followAllButton) + } + + @Test + fun `genre creator adapter sizes group card to parent width minus horizontal padding`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 402.dpToPx(), 600.dpToPx()) + + val viewHolder = HomeGenreCreatorAdapter(onFollowAllClick = {}).onCreateViewHolder(parent, 0) + + assertEquals(374.dpToPx(), viewHolder.itemView.layoutParams.width) + } + + @Test + fun `genre creator adapter sizes profile cells to fit four columns in card grid`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 360.dpToPx(), 600.dpToPx()) + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = {}) + adapter.submitGroups(listOf(genreGroup("genre", genreCreators(1L)))) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + adapter.onBindViewHolder(viewHolder, 0) + + val creatorGrid = viewHolder.itemView.findViewById(R.id.gl_home_genre_creator_profiles) + val firstProfile = creatorGrid.getChildAt(0) + val expectedCellSize = ((360 - 28 - 28 - 14 * 3) / 4).dpToPx() + + assertEquals(expectedCellSize, firstProfile.layoutParams.width) + } + + @Test + fun `genre creator adapter uses recycler width when group width is not resolved`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 360.dpToPx(), 600.dpToPx()) + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = {}) + adapter.submitGroups(listOf(genreGroup("genre", genreCreators(1L)))) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + viewHolder.itemView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + + adapter.onBindViewHolder(viewHolder, 0) + + val creatorGrid = viewHolder.itemView.findViewById(R.id.gl_home_genre_creator_profiles) + val firstProfile = creatorGrid.getChildAt(0) + val expectedCellSize = ((360 - 28 - 28 - 14 * 3) / 4).dpToPx() + + assertEquals(expectedCellSize, firstProfile.layoutParams.width) + } + + @Test + fun `genre creator adapter caps measured group width to recycler content width`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.setPadding(14.dpToPx(), 0, 14.dpToPx(), 0) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 402.dpToPx(), 600.dpToPx()) + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = {}) + adapter.submitGroups(listOf(genreGroup("genre", genreCreators(1L)))) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + viewHolder.itemView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + viewHolder.itemView.layout(0, 0, 402.dpToPx(), 300.dpToPx()) + + adapter.onBindViewHolder(viewHolder, 0) + + val creatorGrid = viewHolder.itemView.findViewById(R.id.gl_home_genre_creator_profiles) + val firstProfile = creatorGrid.getChildAt(0) + val expectedCellSize = ((402 - 28 - 28 - 14 * 3) / 4).dpToPx() + + assertEquals(expectedCellSize, firstProfile.layoutParams.width) + } + + @Test + fun `genre creator adapter uses measured grid width for profile cells`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 402.dpToPx(), 600.dpToPx()) + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = {}) + adapter.submitGroups(listOf(genreGroup("genre", genreCreators(1L)))) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + val creatorGrid = viewHolder.itemView.findViewById(R.id.gl_home_genre_creator_profiles) + viewHolder.itemView.layout(0, 0, 374.dpToPx(), 300.dpToPx()) + creatorGrid.layout(0, 0, 300.dpToPx(), 200.dpToPx()) + + adapter.onBindViewHolder(viewHolder, 0) + + val firstProfile = creatorGrid.getChildAt(0) + val expectedCellSize = ((300 - 14 * 3) / 4).dpToPx() + + assertEquals(expectedCellSize, firstProfile.layoutParams.width) + } + + @Test + fun `genre creator suffix preserves leading spaces only for spaced languages`() { + val context = ApplicationProvider.getApplicationContext() + val englishContext = context.createConfigurationContext( + Configuration(context.resources.configuration).apply { setLocale(Locale.ENGLISH) } + ) + val japaneseContext = context.createConfigurationContext( + Configuration(context.resources.configuration).apply { setLocale(Locale.JAPANESE) } + ) + + assertEquals(true, context.getString(R.string.home_recommendation_section_genre_creator_suffix).startsWith(" ")) + assertEquals(true, englishContext.getString(R.string.home_recommendation_section_genre_creator_suffix).startsWith(" ")) + assertEquals(false, japaneseContext.getString(R.string.home_recommendation_section_genre_creator_suffix).startsWith(" ")) + } + + @Test + fun `genre creator profile item uses parent cell width and square image ratio`() { + val profile = inflateViewWithParent(R.layout.item_home_genre_creator_profile) + val profileImage = profile.findViewById(R.id.iv_home_genre_creator_profile) + val nickname = profile.findViewById(R.id.tv_home_genre_creator_profile_nickname) + val imageLayoutParams = profileImage.layoutParams as ConstraintLayout.LayoutParams + + assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, profile.layoutParams.width) + assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, profile.layoutParams.height) + assertEquals(0, imageLayoutParams.width) + assertEquals(0, imageLayoutParams.height) + assertEquals("1:1", imageLayoutParams.dimensionRatio) + assertEquals(6.dpToPx(), (nickname.layoutParams as ViewGroup.MarginLayoutParams).topMargin) + assertEquals(14f, nickname.textSize / nickname.resources.displayMetrics.scaledDensity) + } + + @Test + fun `home genre creator section uses pager snapping list`() { + val root = inflateView(R.layout.fragment_v2_main_home) + val genreList = root.findViewById(R.id.rv_home_genre_creators) + + assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, genreList.layoutParams.height) + assertEquals(14.dpToPx(), genreList.paddingStart) + assertEquals(14.dpToPx(), genreList.paddingEnd) + assertNotNull(genreList.onFlingListener) + } + + @Test + fun `genre creator adapter renders max five non empty genre groups`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = {}) + + adapter.submitGroups( + listOf( + genreGroup("empty", emptyList()), + genreGroup("genre1", genreCreators(1L)), + genreGroup("genre2", genreCreators(11L)), + genreGroup("genre3", genreCreators(21L)), + genreGroup("genre4", genreCreators(31L)), + genreGroup("genre5", genreCreators(41L)), + genreGroup("genre6", genreCreators(51L)) + ) + ) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + adapter.onBindViewHolder(viewHolder, 0) + + assertEquals(5, adapter.itemCount) + assertEquals("genre1", viewHolder.itemView.findViewById(R.id.tv_home_genre_creator_group_title_genre).text) + assertEquals(8, viewHolder.itemView.findViewById(R.id.gl_home_genre_creator_profiles).childCount) + } + + @Test + fun `genre creator adapter follow all uses ids from its page group`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + val clickedCreatorIds = mutableListOf() + val adapter = HomeGenreCreatorAdapter(onFollowAllClick = { clickedCreatorIds.addAll(it) }) + adapter.submitGroups(listOf(genreGroup("genre", genreCreators(10L)))) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + adapter.onBindViewHolder(viewHolder, 0) + viewHolder.itemView.findViewById(R.id.view_home_genre_group_follow_all).performClick() + + assertEquals((10L..17L).toList(), clickedCreatorIds) + } + + @Test + fun `genre creator visible groups hide all empty groups before fragment visibility`() { + val section = HomeRecommendationGenreCreatorSection( + listOf( + genreGroup("empty1", emptyList()), + genreGroup("empty2", emptyList()) + ) + ) + + assertEquals(emptyList(), section.visibleHomeGenreCreatorGroups()) + } + @Test fun `first audio adapter clips thumbnail container`() { val context = ApplicationProvider.getApplicationContext() @@ -414,7 +636,7 @@ class HomeMainFragmentLayoutTest { } @Test - fun `home layout uses section title components and custom genre title row`() { + fun `home layout uses section title components and genre title inside page card`() { val root = inflateView(R.layout.fragment_v2_main_home) val sectionTitleIds = listOf( R.id.view_home_recent_activity_title, @@ -437,9 +659,10 @@ class HomeMainFragmentLayoutTest { assertNotNull(chevron) } - assertNotNull(root.findViewById(R.id.ll_home_genre_creator_title)) - assertNotNull(root.findViewById(R.id.tv_home_genre_creator_title_genre)) - assertNotNull(root.findViewById(R.id.tv_home_genre_creator_title_suffix)) + val genreGroup = inflateView(R.layout.item_home_genre_creator_group) + + assertNotNull(genreGroup.findViewById(R.id.tv_home_genre_creator_group_title_genre)) + assertNotNull(genreGroup.findViewById(R.id.tv_home_genre_creator_group_title_suffix)) } @Test @@ -512,4 +735,21 @@ class HomeMainFragmentLayoutTest { tags = setOf(AudioContentTag.First, AudioContentTag.Point, AudioContentTag.Free) ) } + + private fun genreGroup( + genre: String, + creators: List + ): HomeRecommendationGenreCreatorGroupUiModel { + return HomeRecommendationGenreCreatorGroupUiModel(genre, creators) + } + + private fun genreCreators(startId: Long): List { + return (startId until startId + 8L).map { creatorId -> + HomeRecommendationCreatorUiModel( + creatorId = creatorId, + nickname = "크리에이터$creatorId", + profileImage = null + ) + } + } }