Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package com.flint.android.presentation.onboarding

import androidx.activity.ComponentActivity
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.flint.android.core.common.util.UiState
import com.flint.android.core.designsystem.theme.FlintTheme
import com.flint.android.domain.model.terms.TermModel
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* 약관 동의 상태가 화면 이동 후에도 유지되는지 실제 NavHost로 검증한다.
*
* QA 2차 1: 약관 동의 후 '자세히 보기' 진입 → 복귀 시 동의 상태 유지
* QA 2차 2: 약관 동의 후 온보딩 진입 → 뒤로가기 복귀 시 동의 상태 유지
*
* 프로덕션과 동일하게 상태를 화면 바깥(그래프 스코프)에 두고 실제로 이동/복귀시킨다.
* 수정 전에는 체크 상태가 OnboardingTermsScreen 내부 remember에 있어,
* 복귀 시 컴포지션이 새로 만들어지며 초기화됐다.
*/
@RunWith(AndroidJUnit4::class)
class OnboardingTermsNavigationTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()

private val service =
TermModel(
id = "1",
type = "SERVICE",
version = 1,
title = "서비스 이용약관",
content = "",
required = true,
activeAt = "2026-05-13T10:48:54.554Z",
)
private val privacy =
TermModel(
id = "2",
type = "PRIVACY",
version = 1,
title = "개인정보 처리 방침",
content = "",
required = true,
activeAt = "2026-05-13T10:48:54.554Z",
)
private val marketing =
TermModel(
id = "3",
type = "MARKETING",
version = 1,
title = "마케팅 정보 수신 동의",
content = "",
required = false,
activeAt = "2026-05-13T10:48:54.554Z",
)

private lateinit var navController: NavHostController

private fun setContent() {
composeRule.setContent {
navController = rememberNavController()

// 프로덕션에서 OnboardingViewModel이 nav graph에 스코프되어 하는 역할.
// NavHost 바깥에 두어 목적지 이동에도 살아남게 한다.
var termsState by remember {
mutableStateOf(
OnboardingTermsUiState(
termsState = UiState.Success(listOf(service, privacy, marketing)),
),
)
}

FlintTheme {
NavHost(navController = navController, startDestination = "terms") {
composable("terms") {
OnboardingTermsScreen(
termsUiState = termsState,
onBackClick = {},
onCheckClick = { termsState = termsState.toggleChecked(it) },
onAllCheckClick = { termsState = termsState.toggleAllChecked() },
onExpandClick = { termsState = termsState.toggleExpanded(it) },
onAgreeClick = { navController.navigate("next") },
onDetailClick = { navController.navigate("detail") },
)
}
composable("detail") { Text("약관 상세 화면") }
composable("next") { Text("온보딩 다음 단계") }
}
}
}
}

private fun goBack() {
composeRule.runOnUiThread { navController.popBackStack() }
composeRule.waitForIdle()
}

private fun checkRequiredTerms() {
composeRule.onNodeWithContentDescription("${service.title} 동의").performClick()
composeRule.onNodeWithContentDescription("${privacy.title} 동의").performClick()
}

// ---- QA 2차 1 ----

@Test
fun `자세히 보기에서 복귀해도 동의 상태가 유지된다`() {
setContent()

checkRequiredTerms()
composeRule.onNodeWithText("동의하기").assertIsEnabled()

// 약관을 펼쳐 '자세히 보기'로 이동
composeRule.onNodeWithContentDescription("${service.title} 펼치기").performClick()
composeRule.onNodeWithText("자세히 보기").performClick()
composeRule.onNodeWithText("약관 상세 화면").assertExists()

goBack()

// 체크와 펼침 상태가 그대로여야 한다
composeRule.onNodeWithText("동의하기").assertIsEnabled()
composeRule.onNodeWithText("자세히 보기").assertExists()
}

// ---- QA 2차 2 ----

@Test
fun `온보딩 다음 단계에서 뒤로가기로 복귀해도 동의 상태가 유지된다`() {
setContent()

checkRequiredTerms()
composeRule.onNodeWithText("동의하기").assertIsEnabled()

composeRule.onNodeWithText("동의하기").performClick()
composeRule.onNodeWithText("온보딩 다음 단계").assertExists()

goBack()

composeRule.onNodeWithText("동의하기").assertIsEnabled()
}

// ---- 전체 동의 ----

@Test
fun `전체 동의 후 이동했다 복귀해도 전체 체크가 유지된다`() {
setContent()

composeRule.onNodeWithText("전체 동의").performClick()
composeRule.onNodeWithText("동의하기").assertIsEnabled()

composeRule.onNodeWithText("동의하기").performClick()
composeRule.onNodeWithText("온보딩 다음 단계").assertExists()

goBack()

composeRule.onNodeWithText("동의하기").assertIsEnabled()
// 선택 약관까지 체크된 상태가 유지되는지 확인 — 다시 누르면 전체 해제되어야 한다
composeRule.onNodeWithText("전체 동의").performClick()
composeRule.onNodeWithText("동의하기").assertIsNotEnabled()
}

// ---- 대조군 ----

@Test
fun `필수 약관을 체크하지 않으면 동의하기가 비활성화된다`() {
setContent()

composeRule.onNodeWithText("동의하기").assertIsNotEnabled()

composeRule.onNodeWithContentDescription("${marketing.title} 동의").performClick()
composeRule.onNodeWithText("동의하기").assertIsNotEnabled()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.flint.android.R
import com.flint.android.core.designsystem.interaction.FlintPressDefaults
Expand All @@ -47,10 +48,14 @@ fun FlintBasicButton(
contentPadding: PaddingValues,
@DrawableRes leadingIconRes: Int? = null,
pressedScale: Float = FlintPressDefaults.BUTTON_SCALE,
// 상태가 정한 테두리 굵기를 화면 단위로 덮어쓸 때만 지정한다. null이면 상태 기본값을 쓴다.
borderWidth: Dp? = null,
) {
val background: Brush = state.background
val contentColor: Color = state.contentColor
val border: BorderStroke? = state.border
val border: BorderStroke? = state.border?.let { stroke ->
if (borderWidth == null) stroke else BorderStroke(borderWidth, stroke.brush)
}
val shape = RoundedCornerShape(8.dp)

val interactionSource = remember { MutableInteractionSource() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.flint.android.core.designsystem.theme.FlintTheme

Expand All @@ -24,6 +25,7 @@ fun FlintMediumButton(
modifier: Modifier = Modifier,
textStyle: TextStyle = if (state == FlintButtonState.Able) FlintTheme.typography.body1Sb16 else FlintTheme.typography.body1M16,
enabled: Boolean = true,
borderWidth: Dp? = null,
) {
FlintBasicButton(
text = text,
Expand All @@ -32,6 +34,7 @@ fun FlintMediumButton(
textStyle = textStyle,
enabled = enabled,
contentPadding = PaddingValues(10.dp),
borderWidth = borderWidth,
modifier =
modifier
.padding(vertical = 2.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private fun HomeScreen(

SavedContentsSection(
title = "최근 저장한 콘텐츠",
description = "현재 구독 중인 OTT에서 볼 수 있는 작품들이에요",
description = "관심이 가는 작품들을 다시 만나보세요",
isAllVisible = false,
onAllClick = {},
contentModelList = savedContentModelList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.rememberNavController
Expand Down Expand Up @@ -110,15 +109,10 @@ class MainNavigator(
launchSingleTop = true
}

fun navigateToLogin() {
navController.navigateToLogin(
navOptions {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
launchSingleTop = true
},
)
// 시작 목적지(Splash)는 홈 진입 시 이미 스택에서 제거되므로 popUpTo(startDestination)으로는
// 아무것도 정리되지 않는다. 로그인 화면에서는 앱으로 되돌아갈 수 없어야 하므로 스택을 전부 비운다.
fun navigateToLogin(navOptions: NavOptions? = clearStackNavOptions) {
navController.navigateToLogin(navOptions)
}

fun navigateToOnBoarding(tempToken: String, navOptions: NavOptions? = clearStackNavOptions) {
Expand Down
Loading
Loading