fix(home): 홈 추천 후속 검증을 반영한다
This commit is contained in:
@@ -20,6 +20,7 @@ import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
||||
import kr.co.vividnext.sodalive.v2.chat.action.ChatActionCommand
|
||||
import kr.co.vividnext.sodalive.v2.chat.action.handleChatAction
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.community.action.CommunityActionCommand
|
||||
import kr.co.vividnext.sodalive.v2.community.action.handleCommunityAction
|
||||
@@ -232,10 +233,26 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
adapter = popularCommunityAdapter
|
||||
}
|
||||
onCheerFollowAllClick = { creatorIds ->
|
||||
homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)
|
||||
ensureV2Access(AccessRequirement.Login) {
|
||||
showCheerCreatorsFollowAllConfirmDialog(creatorIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCheerCreatorsFollowAllConfirmDialog(creatorIds: List<Long>) {
|
||||
V2ModalDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.home_recommendation_follow_all_confirm_title),
|
||||
desc = getString(R.string.home_recommendation_follow_all_confirm_description),
|
||||
confirmButtonTitle = getString(R.string.home_recommendation_follow_all_confirm_button),
|
||||
confirmButtonClick = {
|
||||
homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)
|
||||
},
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun setUpCreatorRankingAdapter() {
|
||||
binding.rvHomeCreatorRankings.apply {
|
||||
layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.dispose
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||
@@ -51,9 +50,8 @@ class HomePopularCommunityAdapter(
|
||||
view.setOnFeedClick { feedItem -> onClickItem(feedItem as FeedItem.Community) }
|
||||
bindImage(item.item.creatorImageUrl, view.profileImageView())
|
||||
bindImage(
|
||||
imageUrl = item.item.imageUrl,
|
||||
imageUrl = item.item.imageUrl.takeIf { !item.item.isLockedPaidPost() },
|
||||
imageView = view.communityImageView(),
|
||||
shouldBlur = item.item.price > 0 && !item.item.existOrdered,
|
||||
onImageLoaded = { width, height -> view.applyCommunityImageSize(width, height) }
|
||||
)
|
||||
}
|
||||
@@ -61,7 +59,6 @@ class HomePopularCommunityAdapter(
|
||||
private fun bindImage(
|
||||
imageUrl: String?,
|
||||
imageView: android.widget.ImageView,
|
||||
shouldBlur: Boolean = false,
|
||||
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||
) {
|
||||
if (imageUrl.isNullOrBlank()) {
|
||||
@@ -74,11 +71,12 @@ class HomePopularCommunityAdapter(
|
||||
onImageLoaded?.invoke(result.drawable.intrinsicWidth, result.drawable.intrinsicHeight)
|
||||
}
|
||||
)
|
||||
if (shouldBlur) {
|
||||
transformations(BlurTransformation(imageView.context, 25f, 2.5f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FeedItem.Community.isLockedPaidPost(): Boolean {
|
||||
return price > 0 && !existOrdered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,9 @@
|
||||
<string name="home_recommendation_activity_community">Community</string>
|
||||
<string name="home_recommendation_follow_all">Follow all</string>
|
||||
<string name="home_recommendation_follow_all_done">Following all</string>
|
||||
<string name="home_recommendation_follow_all_confirm_title">Follow all</string>
|
||||
<string name="home_recommendation_follow_all_confirm_description">Would you like to follow all creators in this list?</string>
|
||||
<string name="home_recommendation_follow_all_confirm_button">Follow</string>
|
||||
<string name="home_recommendation_more">More</string>
|
||||
<string name="home_recommendation_collapse">Collapse</string>
|
||||
<string name="common_error_unknown">An unknown error occurred. try again.</string>
|
||||
|
||||
@@ -323,6 +323,9 @@
|
||||
<string name="home_recommendation_activity_community">コミュニティ</string>
|
||||
<string name="home_recommendation_follow_all">すべてフォロー</string>
|
||||
<string name="home_recommendation_follow_all_done">すべてフォロー中</string>
|
||||
<string name="home_recommendation_follow_all_confirm_title">すべてフォロー</string>
|
||||
<string name="home_recommendation_follow_all_confirm_description">このリストのクリエイターをすべてフォローしますか?</string>
|
||||
<string name="home_recommendation_follow_all_confirm_button">フォローする</string>
|
||||
<string name="home_recommendation_more">もっと見る</string>
|
||||
<string name="home_recommendation_collapse">折りたたむ</string>
|
||||
<string name="common_error_unknown">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string>
|
||||
|
||||
@@ -322,6 +322,9 @@
|
||||
<string name="home_recommendation_activity_community">커뮤니티</string>
|
||||
<string name="home_recommendation_follow_all">모두 팔로우 하기</string>
|
||||
<string name="home_recommendation_follow_all_done">모두 팔로우 완료</string>
|
||||
<string name="home_recommendation_follow_all_confirm_title">모두 팔로우</string>
|
||||
<string name="home_recommendation_follow_all_confirm_description">목록의 크리에이터를 모두 팔로우하시겠어요?</string>
|
||||
<string name="home_recommendation_follow_all_confirm_button">팔로우 하기</string>
|
||||
<string name="home_recommendation_more">더보기</string>
|
||||
<string name="home_recommendation_collapse">접기</string>
|
||||
<string name="common_error_unknown">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string>
|
||||
|
||||
@@ -1022,18 +1022,6 @@ class HomeMainFragmentLayoutTest {
|
||||
assertEquals(null, aiCharacter(creatorId = -1L).toHomeRecommendationAiCharacterRoute())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home popular community adapter applies blur when locked paid post image is loaded`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("imageUrl = item.item.imageUrl"))
|
||||
assertTrue(source.contains("shouldBlur = item.item.price > 0 && !item.item.existOrdered"))
|
||||
assertTrue(source.contains("BlurTransformation(imageView.context, 25f, 2.5f)"))
|
||||
assertFalse(source.contains("item.item.imageUrl.takeIf { item.item.price <= 0 || item.item.existOrdered }"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home live section height follows its items`() {
|
||||
val root = inflateView(R.layout.fragment_v2_main_home)
|
||||
@@ -1298,6 +1286,73 @@ class HomeMainFragmentLayoutTest {
|
||||
assertFalse(source.contains("followCreators(genreSectionKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `응원 크리에이터 모두 팔로우는 API 요청 전에 확인창을 연다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
|
||||
assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog"))
|
||||
assertTrue(source.contains("private fun showCheerCreatorsFollowAllConfirmDialog(creatorIds: List<Long>)"))
|
||||
assertTrue(
|
||||
source.contains(
|
||||
"onCheerFollowAllClick = { creatorIds ->\n" +
|
||||
" ensureV2Access(AccessRequirement.Login) {\n" +
|
||||
" showCheerCreatorsFollowAllConfirmDialog(creatorIds)\n" +
|
||||
" }\n" +
|
||||
" }"
|
||||
)
|
||||
)
|
||||
assertTrue(source.contains("V2ModalDialog("))
|
||||
assertTrue(source.contains("title = getString(R.string.home_recommendation_follow_all_confirm_title)"))
|
||||
assertTrue(source.contains("desc = getString(R.string.home_recommendation_follow_all_confirm_description)"))
|
||||
assertTrue(
|
||||
source.contains(
|
||||
"confirmButtonTitle = getString(R.string.home_recommendation_follow_all_confirm_button)"
|
||||
)
|
||||
)
|
||||
assertTrue(source.contains("cancelButtonTitle = getString(R.string.cancel)"))
|
||||
assertTrue(
|
||||
source.contains(
|
||||
"confirmButtonClick = {\n" +
|
||||
" homeRecommendationViewModel.followCreators(SECTION_KEY_CHEER_CREATORS, creatorIds)\n" +
|
||||
" }"
|
||||
)
|
||||
)
|
||||
assertTrue(source.contains(".show(screenWidth)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `모두 팔로우 확인창 문자열은 다국어로 제공한다`() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val koreanContext = context.createConfigurationContext(
|
||||
Configuration(context.resources.configuration).apply { setLocale(Locale.KOREAN) }
|
||||
)
|
||||
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("모두 팔로우", koreanContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||
assertEquals(
|
||||
"목록의 크리에이터를 모두 팔로우하시겠어요?",
|
||||
koreanContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||
)
|
||||
assertEquals("팔로우 하기", koreanContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||
assertEquals("Follow all", englishContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||
assertEquals(
|
||||
"Would you like to follow all creators in this list?",
|
||||
englishContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||
)
|
||||
assertEquals("Follow", englishContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||
assertEquals("すべてフォロー", japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_title))
|
||||
assertEquals(
|
||||
"このリストのクリエイターをすべてフォローしますか?",
|
||||
japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_description)
|
||||
)
|
||||
assertEquals("フォローする", japaneseContext.getString(R.string.home_recommendation_follow_all_confirm_button))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home recommendation viewmodel does not keep android context`() {
|
||||
val viewModelSource = homeRecommendationViewModelSource()
|
||||
|
||||
@@ -195,6 +195,21 @@ class HomeMainFragmentLoginGuardSourceTest {
|
||||
assertTrue(clickSource.contains("CreatorActionCommand.Profile(route.creatorId)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 응원 크리에이터 모두 팔로우는 확인창보다 로그인 가드를 먼저 실행한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
val setupSource = source.substringFrom("private fun setUpRecommendationAdapters()")
|
||||
assertTrue(setupSource.contains("onCheerFollowAllClick = { creatorIds ->"))
|
||||
assertBefore(
|
||||
setupSource,
|
||||
"ensureV2Access(AccessRequirement.Login)",
|
||||
"showCheerCreatorsFollowAllConfirmDialog(creatorIds)"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
|
||||
val source = projectFile(
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPaidStatus
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class HomePopularCommunityAdapterTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
if (!ImageLoaderProvider.isInitialized) {
|
||||
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
|
||||
}
|
||||
mockkStatic("kr.co.vividnext.sodalive.extensions.ImageExtensionsKt")
|
||||
every { any<ImageView>().loadUrl(any(), any()) } just Runs
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic("kr.co.vividnext.sodalive.extensions.ImageExtensionsKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `유료 미구매 bind는 원본을 로드하지 않고 이전 요청과 drawable을 제거한다`() {
|
||||
val adapter = adapterWith(
|
||||
communityPost(
|
||||
imageUrl = "https://example.com/locked.png",
|
||||
price = 30,
|
||||
existOrdered = false
|
||||
)
|
||||
)
|
||||
val holder = createViewHolder(adapter)
|
||||
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_community_image)
|
||||
val disposable = imageView.load(ColorDrawable(Color.BLUE), ImageLoaderProvider.imageLoader)
|
||||
assertFalse(disposable.isDisposed)
|
||||
|
||||
adapter.onBindViewHolder(holder, 0)
|
||||
|
||||
verify(exactly = 0) { imageView.loadUrl(any(), any()) }
|
||||
assertTrue(disposable.isDisposed)
|
||||
assertNull(imageView.drawable)
|
||||
assertEquals(
|
||||
View.VISIBLE,
|
||||
holder.itemView.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility
|
||||
)
|
||||
assertEquals(
|
||||
"30",
|
||||
holder.itemView.findViewById<TextView>(R.id.tv_feed_community_price).text.toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `무료와 구매 완료 bind는 원본 이미지 URL을 로더에 전달한다`() {
|
||||
val freeImageUrl = "https://example.com/free.png"
|
||||
val purchasedImageUrl = "https://example.com/purchased.png"
|
||||
val adapter = adapterWith(
|
||||
communityPost(imageUrl = freeImageUrl, price = 0, existOrdered = false),
|
||||
communityPost(imageUrl = purchasedImageUrl, price = 30, existOrdered = true)
|
||||
)
|
||||
val holder = createViewHolder(adapter)
|
||||
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_community_image)
|
||||
|
||||
adapter.onBindViewHolder(holder, 0)
|
||||
adapter.onBindViewHolder(holder, 1)
|
||||
|
||||
verify(exactly = 1) { imageView.loadUrl(freeImageUrl, any()) }
|
||||
verify(exactly = 1) { imageView.loadUrl(purchasedImageUrl, any()) }
|
||||
assertEquals(View.GONE, holder.itemView.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
||||
}
|
||||
|
||||
private fun adapterWith(
|
||||
vararg items: HomeRecommendationPopularCommunityPostUiModel
|
||||
): HomePopularCommunityAdapter {
|
||||
return HomePopularCommunityAdapter(onClickItem = {}).apply {
|
||||
submitSection(HomeRecommendationPopularCommunityPostSection(items.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewHolder(
|
||||
adapter: HomePopularCommunityAdapter
|
||||
): HomePopularCommunityAdapter.CommunityViewHolder {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val parent = RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
}
|
||||
return adapter.onCreateViewHolder(parent, 0)
|
||||
}
|
||||
|
||||
private fun communityPost(
|
||||
imageUrl: String,
|
||||
price: Int,
|
||||
existOrdered: Boolean
|
||||
): HomeRecommendationPopularCommunityPostUiModel {
|
||||
val paidStatus = when {
|
||||
price == 0 -> HomeRecommendationPaidStatus.Free
|
||||
existOrdered -> HomeRecommendationPaidStatus.Purchased
|
||||
else -> HomeRecommendationPaidStatus.Paid(price)
|
||||
}
|
||||
return HomeRecommendationPopularCommunityPostUiModel(
|
||||
item = FeedItem.Community(
|
||||
feedId = "feed-1",
|
||||
creatorId = "creator-1",
|
||||
creatorName = "크리에이터",
|
||||
creatorImageUrl = "",
|
||||
postId = "post-1",
|
||||
bodyText = "본문",
|
||||
keywordText = "",
|
||||
createdAtText = "방금 전",
|
||||
commentCount = 1,
|
||||
likeCount = 2,
|
||||
imageUrl = imageUrl,
|
||||
audioUrl = null,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
showKeyword = false
|
||||
),
|
||||
paidStatus = paidStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user