diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 869408005..bada9e639 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,7 @@ android { defaultConfig { applicationId = "com.ku_stacks.ku_ring" + testInstrumentationRunner = "com.ku_stacks.ku_ring.HiltTestRunner" javaCompileOptions { annotationProcessorOptions { @@ -116,6 +117,7 @@ dependencies { implementation(libs.play.services.auth) androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.espresso.intents) + androidTestImplementation(libs.androidx.work.testing) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.crashlytics.ktx) } diff --git a/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/HiltTestRunner.kt b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/HiltTestRunner.kt new file mode 100644 index 000000000..2284fab18 --- /dev/null +++ b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/HiltTestRunner.kt @@ -0,0 +1,32 @@ +package com.ku_stacks.ku_ring + +import android.app.Application +import android.content.Context +import android.util.Log +import androidx.test.runner.AndroidJUnitRunner +import androidx.work.Configuration +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import dagger.hilt.android.testing.CustomTestApplication + +@CustomTestApplication(KuRingTestApplication::class) +interface HiltTestApp + +open class KuRingTestApplication : Application(), Configuration.Provider { + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setExecutor(SynchronousExecutor()) + .build() + + override fun onCreate() { + super.onCreate() + WorkManagerTestInitHelper.initializeTestWorkManager(this, workManagerConfiguration) + } +} + +class HiltTestRunner : AndroidJUnitRunner() { + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApp_Application::class.java.name, context) + } +} diff --git a/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/NavigationE2ETest.kt b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/NavigationE2ETest.kt new file mode 100644 index 000000000..5a78e822f --- /dev/null +++ b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/NavigationE2ETest.kt @@ -0,0 +1,324 @@ +package com.ku_stacks.ku_ring + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ku_stacks.ku_ring.navigation.Navigator +import com.ku_stacks.ku_ring.navigation.keys.ArchiveKey +import com.ku_stacks.ku_ring.navigation.keys.ClubDetailKey +import com.ku_stacks.ku_ring.navigation.keys.ClubSubscriptionKey +import com.ku_stacks.ku_ring.navigation.keys.EditDepartmentsKey +import com.ku_stacks.ku_ring.navigation.keys.EditSubscriptionKey +import com.ku_stacks.ku_ring.navigation.keys.FeedbackKey +import com.ku_stacks.ku_ring.navigation.keys.LibrarySeatKey +import com.ku_stacks.ku_ring.navigation.keys.MainHubKey +import com.ku_stacks.ku_ring.navigation.keys.NoticeWebKey +import com.ku_stacks.ku_ring.navigation.keys.NotificationKey +import com.ku_stacks.ku_ring.navigation.keys.OpenSourceKey +import com.ku_stacks.ku_ring.navigation.keys.SearchKey +import com.ku_stacks.ku_ring.navigation.keys.SplashKey +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import javax.inject.Inject + +/** + * Navigation3 마이그레이션 E2E 테스트. + * + * develop 브랜치 기준의 모든 화면이동 스펙이 현재 브랜치에서도 동일하게 동작하는지 검증한다. + * HostActivity + NavDisplay + Navigator 기반의 실제 앱 환경에서 테스트한다. + */ +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class NavigationE2ETest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + + @Inject + lateinit var navigator: Navigator + + @Before + fun setup() { + hiltRule.inject() + } + + // --- 앱 실행 --- + + @Test + fun 앱이_실행되면_SplashKey가_초기_백스택에_존재한다() { + assertEquals(SplashKey, navigator.backStack.first()) + } + + // --- 공지 탭 네비게이션 --- + + @Test + fun 스플래시에서_메인허브로_이동하면_MainHubKey가_백스택에_존재한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + } + composeTestRule.waitForIdle() + + assertEquals(1, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.first()) + } + + @Test + fun 공지_탭에서_검색으로_이동하면_SearchKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(SearchKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(SearchKey, navigator.backStack.last()) + } + + @Test + fun 공지_탭에서_아카이브로_이동하면_ArchiveKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(ArchiveKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(ArchiveKey, navigator.backStack.last()) + } + + @Test + fun 공지_탭에서_알림_목록으로_이동하면_NotificationKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(NotificationKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(NotificationKey, navigator.backStack.last()) + } + + @Test + fun 공지_탭에서_공지_상세로_이동하면_NoticeWebKey가_백스택_최상단에_위치한다() { + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=5a206c73", + articleId = "5a206c73", + id = "20240315001", + category = "학사", + subject = "2024학년도 1학기 수강신청 안내", + ) + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(noticeKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(noticeKey, navigator.backStack.last()) + } + + @Test + fun 공지_탭에서_학과_편집으로_이동하면_EditDepartmentsKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(EditDepartmentsKey) + + assertEquals(2, navigator.backStack.size) + assertEquals(EditDepartmentsKey, navigator.backStack.last()) + } + } + + // --- 동아리 탭 네비게이션 --- + + @Test + fun 동아리_탭에서_구독_관리로_이동하면_ClubSubscriptionKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(ClubSubscriptionKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(ClubSubscriptionKey, navigator.backStack.last()) + } + + @Test + fun 동아리_탭에서_동아리_상세로_이동하면_ClubDetailKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(ClubDetailKey(clubId = 1)) + + assertEquals(2, navigator.backStack.size) + assertEquals(ClubDetailKey(clubId = 1), navigator.backStack.last()) + } + } + + // --- 설정 탭 네비게이션 --- + + @Test + fun 설정에서_오픈소스_화면으로_이동하면_OpenSourceKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(OpenSourceKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(OpenSourceKey, navigator.backStack.last()) + } + + @Test + fun 설정에서_피드백_화면으로_이동하면_FeedbackKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(FeedbackKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(FeedbackKey, navigator.backStack.last()) + } + + // --- 캠퍼스맵 탭 네비게이션 --- + + @Test + fun 캠퍼스맵에서_열람실_좌석으로_이동하면_LibrarySeatKey가_백스택_최상단에_위치한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(LibrarySeatKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(LibrarySeatKey, navigator.backStack.last()) + } + + // --- 중첩 네비게이션 --- + + @Test + fun 알림_목록에서_구독_편집으로_이동하면_올바른_백스택_순서가_유지된다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(NotificationKey) + navigator.navigate(EditSubscriptionKey) + } + composeTestRule.waitForIdle() + + assertEquals(3, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack[0]) + assertEquals(NotificationKey, navigator.backStack[1]) + assertEquals(EditSubscriptionKey, navigator.backStack[2]) + } + + @Test + fun 구독_편집에서_학과_편집으로_이동하면_올바른_백스택_순서가_유지된다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(EditSubscriptionKey) + navigator.navigate(EditDepartmentsKey) + + assertEquals(3, navigator.backStack.size) + assertEquals(EditSubscriptionKey, navigator.backStack[1]) + assertEquals(EditDepartmentsKey, navigator.backStack[2]) + } + } + + // --- 뒤로가기 --- + + @Test + fun 상세_화면에서_뒤로가기를_하면_이전_화면으로_복원된다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + navigator.navigate(SearchKey) + navigator.navigate( + NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=7b3e9f12", + articleId = "7b3e9f12", + id = "20240401005", + category = "일반", + subject = "2024학년도 건국대학교 학생생활관 입사 안내", + ) + ) + } + composeTestRule.waitForIdle() + assertEquals(3, navigator.backStack.size) + + composeTestRule.runOnUiThread { navigator.goBack() } + composeTestRule.waitForIdle() + assertEquals(SearchKey, navigator.backStack.last()) + + composeTestRule.runOnUiThread { navigator.goBack() } + composeTestRule.waitForIdle() + assertEquals(MainHubKey(), navigator.backStack.last()) + } + + @Test + fun 백스택에_항목이_하나만_남으면_뒤로가기가_실패한다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + } + composeTestRule.waitForIdle() + + composeTestRule.runOnUiThread { + val result = navigator.goBack() + assertEquals(false, result) + } + composeTestRule.waitForIdle() + + assertEquals(1, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.first()) + } + + // --- 딥링크 --- + + @Test + fun FCM_콜드_스타트_시_pendingDeepLink가_스플래시_완료_후_적용된다() { + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=a1c2d3e4", + articleId = "a1c2d3e4", + id = "20240520012", + category = "장학", + subject = "2024학년도 2학기 국가장학금 신청 안내", + ) + + composeTestRule.runOnUiThread { + navigator.setPendingDeepLink(noticeKey) + navigator.replaceAll(MainHubKey()) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack[0]) + assertEquals(noticeKey, navigator.backStack[1]) + } + + @Test + fun FCM_웜_스타트_시_직접_navigate하면_해당_화면이_백스택에_추가된다() { + composeTestRule.runOnUiThread { + navigator.replaceAll(MainHubKey()) + } + composeTestRule.waitForIdle() + + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=f5e6d7c8", + articleId = "f5e6d7c8", + id = "20240601003", + category = "취창업", + subject = "2024 건국대학교 취업박람회 참가 신청 안내", + ) + composeTestRule.runOnUiThread { + navigator.navigate(noticeKey) + } + composeTestRule.waitForIdle() + + assertEquals(2, navigator.backStack.size) + assertEquals(noticeKey, navigator.backStack.last()) + } +} diff --git a/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/SmokeTest.kt b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/SmokeTest.kt index 760c23fa3..a6d1c9fc5 100644 --- a/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/SmokeTest.kt +++ b/app/src/androidTest/kotlin/com/ku_stacks/ku_ring/SmokeTest.kt @@ -1,23 +1,44 @@ package com.ku_stacks.ku_ring import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ku_stacks.ku_ring.splash.SplashActivity +import com.ku_stacks.ku_ring.navigation.Navigator +import com.ku_stacks.ku_ring.navigation.keys.SplashKey +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import javax.inject.Inject +@HiltAndroidTest @RunWith(AndroidJUnit4::class) class SmokeTest { - @get:Rule - val composeTestRule = createAndroidComposeRule() + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + + @Inject + lateinit var navigator: Navigator + + @Before + fun setup() { + hiltRule.inject() + } + + @Test + fun 앱이_실행되면_HostActivity가_정상적으로_생성된다() { + assertTrue(composeTestRule.activity is HostActivity) + } @Test - fun testAppLaunch() { - composeTestRule - .onNodeWithContentDescription("스플래시 화면") - .assertExists() + fun 앱이_실행되면_초기_백스택에_SplashKey가_존재한다() { + assertEquals(SplashKey, navigator.backStack.first()) } } diff --git a/app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt b/app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt index 846e5b98e..192c03135 100644 --- a/app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt +++ b/app/src/main/java/com/ku_stacks/ku_ring/HostActivity.kt @@ -17,6 +17,7 @@ import com.ku_stacks.ku_ring.navigation.DeepLinkIntentFactory import com.ku_stacks.ku_ring.navigation.Navigator import com.ku_stacks.ku_ring.navigation.keys.MainHubKey import com.ku_stacks.ku_ring.navigation.keys.NoticeWebKey +import androidx.annotation.VisibleForTesting import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -73,29 +74,35 @@ class HostActivity : ComponentActivity() { } } - private fun extractNavKey(intent: Intent): NavKey? { - // WebViewNotice가 Intent extras에 있으면 해당 화면으로 이동 - val webViewNotice = IntentCompat.getSerializableExtra( - intent, - WebViewNotice.EXTRA_KEY, - WebViewNotice::class.java, - ) - if (webViewNotice != null) { - return NoticeWebKey( - url = webViewNotice.url, - articleId = webViewNotice.articleId, - id = webViewNotice.id.toString(), - category = webViewNotice.category, - subject = webViewNotice.subject, + private fun extractNavKey(intent: Intent): NavKey? = + extractNavKeyFromIntent(intent) + + companion object { + @VisibleForTesting + internal fun extractNavKeyFromIntent(intent: Intent): NavKey? { + // WebViewNotice가 Intent extras에 있으면 해당 화면으로 이동 + val webViewNotice = IntentCompat.getSerializableExtra( + intent, + WebViewNotice.EXTRA_KEY, + WebViewNotice::class.java, ) - } + if (webViewNotice != null) { + return NoticeWebKey( + url = webViewNotice.url, + articleId = webViewNotice.articleId, + id = webViewNotice.id.toString(), + category = webViewNotice.category, + subject = webViewNotice.subject, + ) + } - // MainScreenRoute가 Intent extras에 있으면 해당 탭으로 이동 - val route = intent.getStringExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE) - if (route != null) { - return MainHubKey(startTab = route) - } + // MainScreenRoute가 Intent extras에 있으면 해당 탭으로 이동 + val route = intent.getStringExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE) + if (route != null) { + return MainHubKey(startTab = route) + } - return null + return null + } } } diff --git a/app/src/test/java/com/ku_stacks/ku_ring/ExtractNavKeyTest.kt b/app/src/test/java/com/ku_stacks/ku_ring/ExtractNavKeyTest.kt new file mode 100644 index 000000000..00486b8bb --- /dev/null +++ b/app/src/test/java/com/ku_stacks/ku_ring/ExtractNavKeyTest.kt @@ -0,0 +1,114 @@ +package com.ku_stacks.ku_ring + +import android.app.Application +import android.content.Intent +import com.ku_stacks.ku_ring.domain.WebViewNotice +import com.ku_stacks.ku_ring.navigation.DeepLinkIntentFactory +import com.ku_stacks.ku_ring.navigation.keys.MainHubKey +import com.ku_stacks.ku_ring.navigation.keys.NoticeWebKey +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class ExtractNavKeyTest { + + @Test + fun `빈 Intent를 전달하면 null을 반환한다`() { + val intent = Intent() + val result = HostActivity.extractNavKeyFromIntent(intent) + assertNull(result) + } + + @Test + fun `WebViewNotice가 포함된 Intent를 전달하면 NoticeWebKey를 반환한다`() { + val notice = WebViewNotice( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=5a206c73", + articleId = "5a206c73", + id = 20240315, + category = "학사", + subject = "2024학년도 1학기 수강신청 안내", + ) + val intent = Intent().apply { + putExtra(WebViewNotice.EXTRA_KEY, notice) + } + + val result = HostActivity.extractNavKeyFromIntent(intent) + + assertEquals( + NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=5a206c73", + articleId = "5a206c73", + id = "20240315", + category = "학사", + subject = "2024학년도 1학기 수강신청 안내", + ), + result, + ) + } + + @Test + fun `MainScreenRoute 문자열이 포함된 Intent를 전달하면 MainHubKey를 반환한다`() { + val intent = Intent().apply { + putExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE, "Calendar") + } + + val result = HostActivity.extractNavKeyFromIntent(intent) + + assertEquals(MainHubKey(startTab = "Calendar"), result) + } + + @Test + fun `WebViewNotice와 Route가 모두 포함되면 WebViewNotice가 우선 적용된다`() { + val notice = WebViewNotice( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=7b3e9f12", + articleId = "7b3e9f12", + id = 20240401, + category = "일반", + subject = "2024학년도 건국대학교 학생생활관 입사 안내", + ) + val intent = Intent().apply { + putExtra(WebViewNotice.EXTRA_KEY, notice) + putExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE, "Calendar") + } + + val result = HostActivity.extractNavKeyFromIntent(intent) + + assertEquals( + NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=7b3e9f12", + articleId = "7b3e9f12", + id = "20240401", + category = "일반", + subject = "2024학년도 건국대학교 학생생활관 입사 안내", + ), + result, + ) + } + + @Test + fun `Notice 탭 Route를 전달하면 Notice startTab의 MainHubKey를 반환한다`() { + val intent = Intent().apply { + putExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE, "Notice") + } + + val result = HostActivity.extractNavKeyFromIntent(intent) + + assertEquals(MainHubKey(startTab = "Notice"), result) + } + + @Test + fun `Club 탭 Route를 전달하면 Club startTab의 MainHubKey를 반환한다`() { + val intent = Intent().apply { + putExtra(DeepLinkIntentFactory.INTENT_KEY_ROUTE, "Club") + } + + val result = HostActivity.extractNavKeyFromIntent(intent) + + assertEquals(MainHubKey(startTab = "Club"), result) + } +} diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 30a928573..e618b404a 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -2,6 +2,7 @@ import com.ku_stacks.ku_ring.buildlogic.dsl.setNameSpace plugins { kuring("feature") + kuringPrimitive("test") } android { diff --git a/core/navigation/src/test/java/com/ku_stacks/ku_ring/navigation/NavigatorTest.kt b/core/navigation/src/test/java/com/ku_stacks/ku_ring/navigation/NavigatorTest.kt new file mode 100644 index 000000000..22c4d5721 --- /dev/null +++ b/core/navigation/src/test/java/com/ku_stacks/ku_ring/navigation/NavigatorTest.kt @@ -0,0 +1,270 @@ +package com.ku_stacks.ku_ring.navigation + +import com.ku_stacks.ku_ring.navigation.keys.ArchiveKey +import com.ku_stacks.ku_ring.navigation.keys.EditDepartmentsKey +import com.ku_stacks.ku_ring.navigation.keys.EditSubscriptionKey +import com.ku_stacks.ku_ring.navigation.keys.FeedbackKey +import com.ku_stacks.ku_ring.navigation.keys.MainHubKey +import com.ku_stacks.ku_ring.navigation.keys.NoticeWebKey +import com.ku_stacks.ku_ring.navigation.keys.NotificationKey +import com.ku_stacks.ku_ring.navigation.keys.SearchKey +import com.ku_stacks.ku_ring.navigation.keys.SplashKey +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class NavigatorTest { + + private lateinit var navigator: Navigator + + @Before + fun setup() { + navigator = Navigator() + } + + // --- 기본 백스택 동작 --- + + @Test + fun `Navigator가 생성되면 SplashKey가 초기 백스택에 존재한다`() { + assertEquals(1, navigator.backStack.size) + assertEquals(SplashKey, navigator.backStack.first()) + } + + @Test + fun `navigate를 호출하면 백스택에 해당 키가 추가된다`() { + navigator.navigate(MainHubKey()) + + assertEquals(2, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.last()) + } + + @Test + fun `여러 화면을 순차적으로 navigate하면 올바른 순서로 백스택이 쌓인다`() { + navigator.navigate(MainHubKey()) + navigator.navigate(SearchKey) + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=5a206c73", + articleId = "5a206c73", + id = "20240315001", + category = "학사", + subject = "2024학년도 1학기 수강신청 안내", + ) + navigator.navigate(noticeKey) + + assertEquals(4, navigator.backStack.size) + assertEquals(SplashKey, navigator.backStack[0]) + assertEquals(MainHubKey(), navigator.backStack[1]) + assertEquals(SearchKey, navigator.backStack[2]) + assertEquals(noticeKey, navigator.backStack[3]) + } + + @Test + fun `goBack을 호출하면 마지막 항목이 제거되고 true를 반환한다`() { + navigator.navigate(MainHubKey()) + navigator.navigate(SearchKey) + + val result = navigator.goBack() + + assertTrue(result) + assertEquals(2, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.last()) + } + + @Test + fun `백스택에 항목이 하나만 남으면 goBack은 false를 반환한다`() { + val result = navigator.goBack() + + assertFalse(result) + assertEquals(1, navigator.backStack.size) + assertEquals(SplashKey, navigator.backStack.first()) + } + + @Test + fun `replaceAll을 호출하면 백스택이 새 키 하나로 교체된다`() { + navigator.navigate(MainHubKey()) + navigator.navigate(SearchKey) + + navigator.replaceAll(MainHubKey()) + + assertEquals(1, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.first()) + } + + // --- 딥링크 --- + + @Test + fun `setPendingDeepLink 후 replaceAll하면 딥링크 키가 백스택에 추가된다`() { + val deepLinkKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=a1c2d3e4", + articleId = "a1c2d3e4", + id = "20240520012", + category = "장학", + subject = "2024학년도 2학기 국가장학금 신청 안내", + ) + navigator.setPendingDeepLink(deepLinkKey) + + navigator.replaceAll(MainHubKey()) + + assertEquals(2, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack[0]) + assertEquals(deepLinkKey, navigator.backStack[1]) + } + + @Test + fun `replaceAll 이후 pendingDeepLink는 소비되어 다음 replaceAll에 영향을 주지 않는다`() { + val deepLinkKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=a1c2d3e4", + articleId = "a1c2d3e4", + id = "20240520012", + category = "장학", + subject = "2024학년도 2학기 국가장학금 신청 안내", + ) + navigator.setPendingDeepLink(deepLinkKey) + + navigator.replaceAll(MainHubKey()) + navigator.replaceAll(MainHubKey()) + + assertEquals(1, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack.first()) + } + + // --- develop 브랜치 기준 네비게이션 경로 검증 --- + + @Test + fun `공지 탭에서 검색 화면으로 이동하면 SearchKey가 백스택 최상단에 위치한다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(SearchKey) + + assertEquals(SearchKey, navigator.backStack.last()) + } + + @Test + fun `공지 탭에서 아카이브 화면으로 이동하면 ArchiveKey가 백스택 최상단에 위치한다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(ArchiveKey) + + assertEquals(ArchiveKey, navigator.backStack.last()) + } + + @Test + fun `공지 탭에서 알림 목록으로 이동하면 NotificationKey가 백스택 최상단에 위치한다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(NotificationKey) + + assertEquals(NotificationKey, navigator.backStack.last()) + } + + @Test + fun `공지 탭에서 공지 상세로 이동하면 NoticeWebKey가 백스택 최상단에 위치한다`() { + navigator.replaceAll(MainHubKey()) + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=7b3e9f12", + articleId = "7b3e9f12", + id = "20240401005", + category = "일반", + subject = "2024학년도 건국대학교 학생생활관 입사 안내", + ) + navigator.navigate(noticeKey) + + assertEquals(noticeKey, navigator.backStack.last()) + } + + @Test + fun `공지 탭에서 학과 편집으로 이동하면 EditDepartmentsKey가 백스택 최상단에 위치한다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(EditDepartmentsKey) + + assertEquals(EditDepartmentsKey, navigator.backStack.last()) + } + + @Test + fun `알림 목록에서 구독 편집으로 이동하면 올바른 백스택 순서가 유지된다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(NotificationKey) + navigator.navigate(EditSubscriptionKey) + + assertEquals(3, navigator.backStack.size) + assertEquals(EditSubscriptionKey, navigator.backStack.last()) + } + + @Test + fun `구독 편집에서 학과 편집으로 이동하면 올바른 백스택 순서가 유지된다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(EditSubscriptionKey) + navigator.navigate(EditDepartmentsKey) + + assertEquals(3, navigator.backStack.size) + assertEquals(EditDepartmentsKey, navigator.backStack.last()) + } + + @Test + fun `상세 화면에서 뒤로가기를 하면 이전 화면으로 복원된다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(SearchKey) + navigator.navigate( + NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=c4d5e6f7", + articleId = "c4d5e6f7", + id = "20240410008", + category = "학생", + subject = "2024학년도 동아리 등록 및 지원금 신청 안내", + ) + ) + + navigator.goBack() + assertEquals(SearchKey, navigator.backStack.last()) + + navigator.goBack() + assertEquals(MainHubKey(), navigator.backStack.last()) + } + + @Test + fun `설정에서 피드백 화면으로 이동 후 뒤로가기하면 메인 허브로 돌아간다`() { + navigator.replaceAll(MainHubKey()) + navigator.navigate(FeedbackKey) + + assertEquals(FeedbackKey, navigator.backStack.last()) + navigator.goBack() + assertEquals(MainHubKey(), navigator.backStack.last()) + } + + @Test + fun `FCM 콜드 스타트 시 pendingDeepLink가 스플래시 완료 후 적용된다`() { + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=e8f9a0b1", + articleId = "e8f9a0b1", + id = "20240601001", + category = "학사", + subject = "2024학년도 1학기 기말고사 시간표 안내", + ) + navigator.setPendingDeepLink(noticeKey) + + navigator.replaceAll(MainHubKey()) + + assertEquals(2, navigator.backStack.size) + assertEquals(MainHubKey(), navigator.backStack[0]) + assertEquals(noticeKey, navigator.backStack[1]) + } + + @Test + fun `웜 스타트 시 직접 navigate하면 해당 화면이 백스택에 추가된다`() { + navigator.replaceAll(MainHubKey()) + + val noticeKey = NoticeWebKey( + url = "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=b2c3d4e5", + articleId = "b2c3d4e5", + id = "20240715003", + category = "장학", + subject = "2024학년도 교내 성적우수 장학금 선발 안내", + ) + navigator.navigate(noticeKey) + + assertEquals(2, navigator.backStack.size) + assertEquals(noticeKey, navigator.backStack.last()) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 172e9c247..dd285fd93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -230,10 +230,6 @@ compose = [ 'compose-material-icons-core', 'compose-material-icons-extended', 'compose-ui', - 'compose-ui-test', - 'compose-ui-test-junit4', - 'compose-ui-test-manifest', - 'compose-ui-tooling', 'compose-ui-tooling-preview', 'androidx-navigation-compose', ]