diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/AccountSettingsScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/AccountSettingsScreenTest.kt new file mode 100644 index 00000000..9eb17f08 --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/AccountSettingsScreenTest.kt @@ -0,0 +1,304 @@ +package com.example.speechbuddy + +import android.content.Intent +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.example.speechbuddy.compose.settings.AccountSettings +import com.example.speechbuddy.ui.SpeechBuddyTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.DelicateCoroutinesApi +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class AccountSettingsScreenTest { + + private val androidTestUtil = AndroidTestUtil() + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = androidTestUtil.createAndroidIntentComposeRule { + Intent(it, HomeActivity::class.java).apply { + putExtra("isTest", true) + } + } + + @Before + fun setUp() { + hiltRule.inject() + val fakeId = 1 + val fakeEmail = "email@email.com" + val fakeNickname = "nickname" + composeTestRule.activity.sessionManager.setUserId(fakeId) + composeTestRule.activity.userRepository.setMyInfo(fakeId, fakeEmail, fakeNickname) + composeTestRule.activity.setContent { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { + AccountSettings( + paddingValues = PaddingValues() + ) + } + } + } + + @Test + fun should_display_all_elements_when_account_settings_screen_appears() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(ACCOUNT).assertIsDisplayed() + composeTestRule.onNodeWithText(EMAIL).assertIsDisplayed() + composeTestRule.onNodeWithText(NICKNAME).assertIsDisplayed() + composeTestRule.onNodeWithText(LOGOUT).assertIsDisplayed().assertHasClickAction().assertIsEnabled() + composeTestRule.onNodeWithText(WITHDRAW).assertIsDisplayed().assertHasClickAction().assertIsEnabled() + composeTestRule.onNodeWithText(FAKE_EMAIL).assertIsDisplayed() + composeTestRule.onNodeWithText(FAKE_NICKNAME).assertIsDisplayed() + } + + @Test + fun should_display_backup_dialog_when_clicking_backup_button() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onNodeWithText(WITHDRAW).assertIsNotEnabled() + composeTestRule.onNodeWithText(BACKUP).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(BACKUP_DIALOG).assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertIsDisplayed().assertHasClickAction() + } + + @Test + fun should_remove_dialog_when_clicking_backup_button_in_backup_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(LOGOUT).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(BACKUP).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(BACKUP_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(BACKUP).assertDoesNotExist() + composeTestRule.onNodeWithText(LOGOUT).assertIsNotEnabled() + composeTestRule.onNodeWithText(WITHDRAW).assertIsNotEnabled() + } + + @Test + fun should_display_logout_dialog_when_clicking_logout_button_in_backup_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(LOGOUT).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[2].performClick() + composeTestRule.waitUntil { + composeTestRule.onAllNodesWithText(LOGOUT_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(CANCEL).assertIsDisplayed() + composeTestRule.onNodeWithText(LOGOUT_DIALOG).assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onNodeWithText(WITHDRAW).assertIsNotEnabled() + } + + @Test + fun should_remove_dialog_when_clicking_logout_button_in_logout_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(LOGOUT).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[2].performClick() + composeTestRule.waitUntil { + composeTestRule.onAllNodesWithText(LOGOUT_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[2].performClick() + composeTestRule.waitUntil { + composeTestRule.onAllNodesWithText(LOGOUT_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(LOGOUT_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(CANCEL).assertDoesNotExist() + } + + @Test + fun should_display_account_settings_screen_when_clicking_cancel_button_in_logout_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(LOGOUT).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(LOGOUT).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(BACKUP_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(LOGOUT)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(LOGOUT)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onAllNodesWithText(LOGOUT)[2].performClick() + composeTestRule.onNodeWithText(CANCEL).performClick() + composeTestRule.waitUntil { + composeTestRule.onAllNodesWithText(LOGOUT_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(LOGOUT_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(CANCEL).assertDoesNotExist() + } + + @Test + fun should_display_first_withdraw_dialog_when_clicking_withdraw_button() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(WITHDRAW).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(WITHDRAW).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(FIRST_WITHDRAW_DIALOG).assertIsDisplayed() + composeTestRule.onNodeWithText(CANCEL).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(PROCEED).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(LOGOUT).assertIsNotEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[1].assertIsEnabled() + } + + @Test + fun should_display_second_withdraw_dialog_when_clicking_progress_button_in_first_withdraw_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(WITHDRAW).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(WITHDRAW).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(PROCEED).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(SECOND_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(SECOND_WITHDRAW_DIALOG).assertIsDisplayed() + composeTestRule.onNodeWithText(CANCEL).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(LOGOUT).assertIsNotEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(WITHDRAW)[2].assertHasClickAction().assertIsEnabled() + } + + @Test + fun should_remove_dialog_when_clicking_cancel_button_in_first_withdraw_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(WITHDRAW).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(WITHDRAW).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(CANCEL).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(FIRST_WITHDRAW_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(CANCEL).assertDoesNotExist() + composeTestRule.onNodeWithText(LOGOUT).assertIsEnabled() + composeTestRule.onNodeWithText(WITHDRAW).assertIsEnabled() + } + + @Test + fun should_remove_dialog_when_clicking_withdraw_button_in_second_withdraw_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(WITHDRAW).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(WITHDRAW).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(PROCEED).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(SECOND_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onAllNodesWithText(WITHDRAW)[0].assertIsNotEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[1].assertIsDisplayed() + composeTestRule.onAllNodesWithText(WITHDRAW)[2].assertHasClickAction().assertIsEnabled() + composeTestRule.onAllNodesWithText(WITHDRAW)[2].performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(SECOND_WITHDRAW_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(SECOND_WITHDRAW_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(CANCEL).assertDoesNotExist() + } + + @Test + fun should_display_account_settings_screen_when_clicking_cancel_button_in_second_withdraw_dialog() { + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(WITHDRAW).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(WITHDRAW).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(FIRST_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(PROCEED).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(SECOND_WITHDRAW_DIALOG).fetchSemanticsNodes().size == 1 + } + composeTestRule.onNodeWithText(CANCEL).performClick() + composeTestRule.waitUntil{ + composeTestRule.onAllNodesWithText(SECOND_WITHDRAW_DIALOG).fetchSemanticsNodes().isEmpty() + } + composeTestRule.onNodeWithText(SECOND_WITHDRAW_DIALOG).assertDoesNotExist() + composeTestRule.onNodeWithText(CANCEL).assertDoesNotExist() + } + + @After + fun tearDown() { + composeTestRule.activityRule.scenario.close() + } + + companion object { + const val ACCOUNT = "계정" + const val EMAIL = "이메일" + const val NICKNAME = "닉네임" + const val LOGOUT = "로그아웃" + const val WITHDRAW = "회원탈퇴" + const val FAKE_EMAIL = "email@email.com" + const val FAKE_NICKNAME = "nickname" + const val BACKUP = "백업" + const val BACKUP_DIALOG = "로그아웃하기 전 백업하시겠습니까? 변경사항을 백업하지 않으면 이용 기록이 손실될 수 있습니다." + const val LOGOUT_DIALOG = "정말로 로그아웃하시겠습니까?" + const val CANCEL = "취소" + const val PROCEED = "진행" + const val FIRST_WITHDRAW_DIALOG = "회원탈퇴를 하면 지금까지의 이용 기록이 모두 사라지며 복구할 수 없습니다. SpeechBuddy를 다시 이용하시려면 게스트 모드를 이용하거나 회원가입을 새로 해야 합니다. 정말로 탈퇴하시겠습니까?" + const val SECOND_WITHDRAW_DIALOG = "정말로 탈퇴하시겠습니까? 이 동작은 취소할 수 없습니다." + } + +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/AndroidTestUtil.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/AndroidTestUtil.kt new file mode 100644 index 00000000..f9741f24 --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/AndroidTestUtil.kt @@ -0,0 +1,39 @@ +package com.example.speechbuddy + +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule + +class AndroidTestUtil { + /** + * Factory method to provide Android specific implementation of createComposeRule, for a given + * activity class type A that needs to be launched via an intent. + * + * @param intentFactory A lambda that provides a Context that can used to create an intent. A intent needs to be returned. + */ + inline fun createAndroidIntentComposeRule(intentFactory: (context: Context) -> Intent) : AndroidComposeTestRule, A> { + val context = ApplicationProvider.getApplicationContext() + val intent = intentFactory(context) + + return AndroidComposeTestRule( + activityRule = ActivityScenarioRule(intent), + activityProvider = { scenarioRule -> scenarioRule.getActivity() } + ) + } + + /** + * Gets the activity from a scenarioRule. + * + * https://androidx.tech/artifacts/compose.ui/ui-test-junit4/1.0.0-alpha11-source/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt.html + */ + fun ActivityScenarioRule.getActivity(): A { + var activity: A? = null + + scenario.onActivity { activity = it } + + return activity ?: throw IllegalStateException("Activity was not set in the ActivityScenarioRule!") + } +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/BackupSettingsScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/BackupSettingsScreenTest.kt new file mode 100644 index 00000000..02d999cc --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/BackupSettingsScreenTest.kt @@ -0,0 +1,83 @@ +package com.example.speechbuddy + +import android.content.Intent +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.example.speechbuddy.compose.settings.BackupSettings +import com.example.speechbuddy.ui.SpeechBuddyTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class BackupSettingsScreenTest { + private val androidTestUtil = AndroidTestUtil() + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = androidTestUtil.createAndroidIntentComposeRule { + Intent(it, HomeActivity::class.java).apply { + putExtra("isTest", true) + } + } + + @Before + fun setUp() { + hiltRule.inject() + composeTestRule.activity.setContent { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { + BackupSettings( + paddingValues = PaddingValues() + ) + } + } + } + + @Test + fun should_display_all_elements_when_account_settings_screen_appears() { + composeTestRule.onNodeWithText(BACKUP_TO_SERVER).assertIsDisplayed() + composeTestRule.onNodeWithText(LAST_BACKUP_DATE).assertIsDisplayed() + composeTestRule.onNodeWithText(ENABLE_AUTO_BACKUP).assertIsDisplayed() + composeTestRule.onNodeWithText(BACKUP_NOW).assertIsDisplayed().assertIsEnabled().assertHasClickAction() + composeTestRule.onNodeWithTag("auto_backup").assertIsDisplayed().assertIsEnabled() + } + + @Test + fun should_change_auto_backup_when_switch_is_clicked() { + composeTestRule.onNodeWithTag("auto_backup").assertIsOn() + composeTestRule.onNodeWithTag("auto_backup").performClick() + composeTestRule.onNodeWithTag("auto_backup").assertIsOff() + composeTestRule.onNodeWithTag("auto_backup").performClick() + composeTestRule.onNodeWithTag("auto_backup").assertIsOn() + } + + @Test + fun should_show_loading_indicator_when_backup_is_clicked() { + composeTestRule.onNodeWithText(BACKUP_NOW).performClick() + composeTestRule.onNodeWithTag("backup_loading").assertIsDisplayed() + } + + companion object { + const val BACKUP_TO_SERVER = "서버에 백업하기" + const val LAST_BACKUP_DATE = "마지막 백업 날짜" + const val ENABLE_AUTO_BACKUP = "자동 백업 활성화" + const val BACKUP_NOW = "지금 백업하기" + } + +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/DisplaySettingsScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/DisplaySettingsScreenTest.kt new file mode 100644 index 00000000..177c0a28 --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/DisplaySettingsScreenTest.kt @@ -0,0 +1,106 @@ +package com.example.speechbuddy + +import android.content.Intent +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.example.speechbuddy.LoginScreenTest.Companion.assertBackgroundColor +import com.example.speechbuddy.compose.settings.DisplaySettings +import com.example.speechbuddy.ui.SpeechBuddyTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class DisplaySettingsScreenTest { + + private val androidTestUtil = AndroidTestUtil() + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = androidTestUtil.createAndroidIntentComposeRule { + Intent(it, HomeActivity::class.java).apply { + putExtra("isTest", true) + } + } + + @Before + fun setUp() { + hiltRule.inject() + composeTestRule.activity.setContent { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { + DisplaySettings( + paddingValues = PaddingValues() + ) + } + } + } + + @Test + fun should_display_all_elements_when_account_settings_screen_appears() { + composeTestRule.onNodeWithText(DISPLAY).assertIsDisplayed() + composeTestRule.onNodeWithText(DARK_MODE).assertIsDisplayed() + composeTestRule.onNodeWithText(INITIAL_PAGE).assertIsDisplayed() + composeTestRule.onNodeWithText(SYMBOL).assertIsDisplayed() + composeTestRule.onNodeWithText(TTS).assertIsDisplayed() + composeTestRule.onNodeWithTag("dark_mode").assertIsDisplayed().assertIsEnabled() + composeTestRule.onNodeWithTag("initial_page_symbol").assertIsDisplayed().assertIsEnabled() + composeTestRule.onNodeWithTag("initial_page_tts").assertIsDisplayed().assertIsEnabled() + } + + @Test + fun should_change_dark_mode_when_clicking_dark_mode() { + composeTestRule.onNodeWithTag("dark_mode").assertIsOff() + composeTestRule.onNodeWithTag("dark_mode").performClick() + composeTestRule.onNodeWithTag("dark_mode").assertIsOn() + composeTestRule.onNodeWithText(DISPLAY).assertBackgroundColor(DARK_COLOR) + composeTestRule.onNodeWithTag("dark_mode").performClick() + composeTestRule.onNodeWithTag("dark_mode").assertIsOff() + composeTestRule.onNodeWithText(DISPLAY).assertBackgroundColor(LIGHT_COLOR) + } + + @Test + fun should_change_initial_page_when_clicking_initial_page() { + composeTestRule.onNodeWithTag("initial_page_symbol").assertIsSelected() + composeTestRule.onNodeWithTag("initial_page_tts").assertIsNotSelected() + composeTestRule.onNodeWithTag("initial_page_tts").performClick() + composeTestRule.onNodeWithTag("initial_page_symbol").assertIsNotSelected() + composeTestRule.onNodeWithTag("initial_page_tts").assertIsSelected() + composeTestRule.onNodeWithTag("initial_page_symbol").performClick() + composeTestRule.onNodeWithTag("initial_page_symbol").assertIsSelected() + composeTestRule.onNodeWithTag("initial_page_tts").assertIsNotSelected() + } + + @After + fun tearDown() { + composeTestRule.activityRule.scenario.close() + } + + companion object { + const val DISPLAY = "디스플레이 설정" + const val DARK_MODE = "다크 모드" + const val INITIAL_PAGE = "시작 페이지" + const val SYMBOL = "상징으로 말하기" + const val TTS = "음성으로 말하기" + + val LIGHT_COLOR = Color(0xFF000000) + val DARK_COLOR = Color(0xFFFFFFFF) + } +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForResetPWTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForResetPWTest.kt index 5393c639..2ebc0d62 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForResetPWTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForResetPWTest.kt @@ -31,7 +31,10 @@ class EmailVerificationScreenForResetPWTest { hiltRule.inject() composeTestRule.activity.setContent { val fakeSource = "reset_password" - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { EmailVerificationScreen( source = fakeSource, navigateCallback = {} diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForSignupTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForSignupTest.kt index a543e2c8..2e943bbe 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForSignupTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/EmailVerificationScreenForSignupTest.kt @@ -31,7 +31,10 @@ class EmailVerificationScreenForSignupTest { hiltRule.inject() composeTestRule.activity.setContent { val fakeSource = "signup" - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { EmailVerificationScreen( source = fakeSource, navigateCallback = {} diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/GuestSettingsScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/GuestSettingsScreenTest.kt new file mode 100644 index 00000000..03205887 --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/GuestSettingsScreenTest.kt @@ -0,0 +1,70 @@ +package com.example.speechbuddy + +import android.content.Intent +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.onNodeWithText +import com.example.speechbuddy.compose.settings.GuestSettings +import com.example.speechbuddy.ui.SpeechBuddyTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class GuestSettingsScreenTest { + private val androidTestUtil = AndroidTestUtil() + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = androidTestUtil.createAndroidIntentComposeRule { + Intent(it, HomeActivity::class.java).apply { + putExtra("isTest", true) + } + } + + @Before + fun setUp() { + hiltRule.inject() + val guestId = -1 + val fakeEmail = "guest" + val fakeNickname = "guest" + composeTestRule.activity.sessionManager.setUserId(guestId) + composeTestRule.activity.userRepository.setMyInfo(guestId, fakeEmail, fakeNickname) + composeTestRule.activity.setContent { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { + GuestSettings( + paddingValues = PaddingValues() + ) + } + } + } + + @Test + fun should_display_all_elements_when_account_settings_screen_appears() { + composeTestRule.onNodeWithText(ACCOUNT).assertIsDisplayed() + composeTestRule.onNodeWithText(ACCOUNT_GUEST_MODE).assertIsDisplayed() + composeTestRule.onNodeWithText(EXIT).assertIsDisplayed().assertIsEnabled().assertHasClickAction() + } + + @After + fun tearDown() { + composeTestRule.activityRule.scenario.close() + } + + companion object { + const val ACCOUNT = "계정" + const val ACCOUNT_GUEST_MODE = "현재 게스트 모드를 사용 중입니다." + const val EXIT = "게스트 모드에서 나가기" + } +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/LandingScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/LandingScreenTest.kt index 5865f1e8..567b65d6 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/LandingScreenTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/LandingScreenTest.kt @@ -26,7 +26,10 @@ class LandingScreenTest { fun setUp() { hiltRule.inject() composeTestRule.activity.setContent { - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { LandingScreen( onLoginClick = {}, isBackup = false) } } diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/LoginScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/LoginScreenTest.kt index 16ee7158..47790675 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/LoginScreenTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/LoginScreenTest.kt @@ -34,7 +34,10 @@ class LoginScreenTest { fun setUp() { hiltRule.inject() composeTestRule.activity.setContent { - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { LoginScreen( onResetPasswordClick = {}, onSignupClick = {} diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/MainSettingsScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/MainSettingsScreenTest.kt new file mode 100644 index 00000000..3d76c7e4 --- /dev/null +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/MainSettingsScreenTest.kt @@ -0,0 +1,80 @@ +package com.example.speechbuddy + +import android.content.Intent +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.navigation.NavHostController +import com.example.speechbuddy.compose.settings.MainSettings +import com.example.speechbuddy.ui.SpeechBuddyTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class MainSettingsScreenTest { + private val androidTestUtil = AndroidTestUtil() + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = androidTestUtil.createAndroidIntentComposeRule { + Intent(it, HomeActivity::class.java).apply { + putExtra("isTest", true) + } + } + + @Before + fun setUp() { + hiltRule.inject() + val fakeId = 0 + val fakeEmail = "email" + val fakeNickname = "nickname" + composeTestRule.activity.sessionManager.setUserId(fakeId) + composeTestRule.activity.userRepository.setMyInfo(fakeId, fakeEmail, fakeNickname) + composeTestRule.activity.setContent { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { + MainSettings( + paddingValues = PaddingValues(), + navController = NavHostController(composeTestRule.activity.applicationContext) + ) + } + } + } + + @Test + fun should_display_all_elements_when_account_settings_screen_appears_in_guest_mode() { + composeTestRule.onNodeWithText(ACCOUNT).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(DISPLAY).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(MY_SETTINGS).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(BACKUP).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(VERSION).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(DEVELOPER).assertIsDisplayed().assertHasClickAction() + composeTestRule.onNodeWithText(COPYRIGHT).assertIsDisplayed().assertHasClickAction() + } + + @After + fun tearDown() { + composeTestRule.activityRule.scenario.close() + } + + companion object { + const val ACCOUNT = "계정" + const val DISPLAY = "디스플레이" + const val MY_SETTINGS = "상징 목록 관리" + const val BACKUP = "서버에 백업하기" + const val VERSION = "버전 정보" + const val DEVELOPER = "개발자 정보" + const val COPYRIGHT = "저작권 정보" + } + +} \ No newline at end of file diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/ResetPasswordScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/ResetPasswordScreenTest.kt index 42a9b8e8..a077a537 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/ResetPasswordScreenTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/ResetPasswordScreenTest.kt @@ -29,7 +29,10 @@ class ResetPasswordScreenTest { fun setUp() { hiltRule.inject() composeTestRule.activity.setContent { - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { ResetPasswordScreen( navigateToLogin = {} ) diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/SignupScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/SignupScreenTest.kt index 8b44608c..0cde1c25 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/SignupScreenTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/SignupScreenTest.kt @@ -31,7 +31,10 @@ class SignupScreenTest { hiltRule.inject() composeTestRule.activity.setContent { val fakeEmail = "test@example.com" - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { SignupScreen( email = fakeEmail, navigateToLogin = {} diff --git a/frontend/app/src/androidTest/java/com/example/speechbuddy/TextToSpeechScreenTest.kt b/frontend/app/src/androidTest/java/com/example/speechbuddy/TextToSpeechScreenTest.kt index 14ee7187..3f877e97 100644 --- a/frontend/app/src/androidTest/java/com/example/speechbuddy/TextToSpeechScreenTest.kt +++ b/frontend/app/src/androidTest/java/com/example/speechbuddy/TextToSpeechScreenTest.kt @@ -33,9 +33,12 @@ class TextToSpeechScreenTest { fun setUp() { composeTestRule.activity.setContent { hiltRule.inject() - SpeechBuddyTheme { + SpeechBuddyTheme( + settingsRepository = composeTestRule.activity.settingsRepository, + initialDarkMode = false + ) { TextToSpeechScreen( - bottomPaddingValues = PaddingValues(16.dp) + paddingValues = PaddingValues() ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt index 35fc7dfc..640780f9 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt @@ -3,6 +3,7 @@ package com.example.speechbuddy import androidx.appcompat.app.AppCompatActivity import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.repository.SettingsRepository +import com.example.speechbuddy.repository.UserRepository import javax.inject.Inject abstract class BaseActivity : AppCompatActivity() { @@ -13,4 +14,7 @@ abstract class BaseActivity : AppCompatActivity() { @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var userRepository: UserRepository + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt index 0ce505d7..3653bdd2 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt @@ -23,7 +23,6 @@ class HomeActivity : BaseActivity() { @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) if(sessionManager.userId.value==GUEST_ID) { // only activates if it is in guest mode. @@ -48,7 +47,7 @@ class HomeActivity : BaseActivity() { private fun subscribeObservers() { sessionManager.isAuthorized.observe(this) { isAuthorized -> - if (!isAuthorized) navAuthActivity() + if (!isAuthorized && !intent.getBooleanExtra("isTest", false)) navAuthActivity() } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt index 57a7b20a..1dabdcc7 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -70,7 +71,7 @@ fun BackupSettings( Switch( checked = uiState.isAutoBackupEnabled, onCheckedChange = { viewModel.setAutoBackup(it) }, - modifier = Modifier.heightIn(max = 32.dp), + modifier = Modifier.heightIn(max = 32.dp).testTag("auto_backup"), enabled = uiState.buttonEnabled ) } @@ -117,6 +118,7 @@ fun BackupSettings( modifier = Modifier .fillMaxSize() .wrapContentSize() + .testTag("backup_loading") ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt index def6abee..ef3dd848 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -61,7 +62,7 @@ fun DisplaySettings( Switch( checked = uiState.isDarkModeEnabled, onCheckedChange = { viewModel.setDarkMode(it) }, - modifier = Modifier.heightIn(max = 32.dp) + modifier = Modifier.heightIn(max = 32.dp).testTag("dark_mode") ) } ) @@ -101,7 +102,7 @@ fun InitialPageColumn( RadioButton( selected = initialPage == InitialPage.SYMBOL_SELECTION, onClick = { onSelectInitialPage(InitialPage.SYMBOL_SELECTION) }, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp).testTag("initial_page_symbol") ) } } @@ -117,7 +118,7 @@ fun InitialPageColumn( RadioButton( selected = initialPage == InitialPage.TEXT_TO_SPEECH, onClick = { onSelectInitialPage(InitialPage.TEXT_TO_SPEECH) }, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp).testTag("initial_page_tts") ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt index 7cc050ba..70960643 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt @@ -2,6 +2,7 @@ package com.example.speechbuddy.repository import com.example.speechbuddy.data.local.UserDao import com.example.speechbuddy.data.local.UserIdPrefsManager +import com.example.speechbuddy.data.local.models.UserEntity import com.example.speechbuddy.data.local.models.UserMapper import com.example.speechbuddy.data.remote.UserRemoteSource import com.example.speechbuddy.data.remote.models.UserDtoMapper @@ -32,8 +33,12 @@ class UserRepository @Inject constructor( fun getMyInfo(): Flow> { return userDao.getUserById(sessionManager.userId.value!!).map { userEntity -> - if (userEntity != null) Resource.success(userMapper.mapToDomainModel(userEntity)) - else Resource.error("Unable to find user", null) + if (userEntity != null) { + Resource.success(userMapper.mapToDomainModel(userEntity)) + } + else { + Resource.error("Unable to find user", null) + } } } @@ -77,4 +82,11 @@ class UserRepository @Inject constructor( ) } + fun setMyInfo(id: Int, email: String, nickname: String) { + CoroutineScope(Dispatchers.IO).launch { + userIdPrefsManager.saveUserId(id) + userDao.insertUser(UserEntity(id, email, nickname)) + } + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/Theme.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/Theme.kt index dd631673..0943450e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/Theme.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/Theme.kt @@ -84,65 +84,39 @@ private val _darkColorScheme = darkColorScheme( @Composable fun SpeechBuddyTheme( - settingsRepository: SettingsRepository? = null, - initialDarkMode: Boolean? = false, + settingsRepository: SettingsRepository, + initialDarkMode: Boolean, darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, content: @Composable () -> Unit ) { - if (settingsRepository != null) { - val darkModeState by settingsRepository.getDarkModeForChange() - .collectAsState(initialDarkMode) + val darkModeState by settingsRepository.getDarkModeForChange() + .collectAsState(initialDarkMode) - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkModeState == true -> _darkColorScheme - else -> _lightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = - darkTheme - } + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) - } else { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - else -> _lightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = - darkTheme - } + darkModeState -> _darkColorScheme + else -> _lightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = + darkTheme } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) }