package social.grain import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import social.grain.data.auth.SecureStore import social.grain.data.local.Appearance import social.grain.data.local.AppearanceStore import social.grain.data.push.PushManager import social.grain.feature.settings.SettingsScreen import social.grain.feature.settings.SettingsViewModel import social.grain.support.Harness import social.grain.support.awaitContentDescription import social.grain.support.awaitText import social.grain.support.MainDispatcherRule import social.grain.support.appContext import social.grain.ui.theme.GrainTheme /** * Settings, including the two dialogs that guard the irreversible rows. * * Sign-out and account deletion both go through a confirmation, and both of * those are pure composable state — nothing below this can tell whether the * dialog appeared, or whether cancelling it left the account alone. */ @RunWith(AndroidJUnit4::class) class SettingsScreenTest { @get:Rule val mainDispatcher = MainDispatcherRule() @get:Rule val composeRule = createComposeRule() private lateinit var harness: Harness private lateinit var appearance: AppearanceStore @Before fun setUp() { harness = Harness() appearance = AppearanceStore(appContext) appearance.set(Appearance.AUTOMATIC) harness.server.on( "dev.hatk.getPreferences", """{"preferences":{"includeExif":true,"includeLocation":false}}""", ) } @After fun tearDown() { appearance.set(Appearance.AUTOMATIC) harness.tearDown() } private fun show( onBack: () -> Unit = {}, onEditProfile: () -> Unit = {}, onOpenBlocked: () -> Unit = {}, onOpenMuted: () -> Unit = {}, ) { val model = SettingsViewModel( harness.client, harness.auth, PushManager(appContext, harness.client, harness.auth, SecureStore(appContext), harness.scope), appearance, ) composeRule.setContent { GrainTheme { SettingsScreen( onBack = onBack, onEditProfile = onEditProfile, onOpenBlocked = onOpenBlocked, onOpenMuted = onOpenMuted, viewModel = model, ) } } composeRule.waitForIdle() } @Test fun theScreenNamesTheSignedInAccountAndItsSections() { show() composeRule.awaitText("Settings") composeRule.onNodeWithText(harness.handle).assertExists() composeRule.awaitText("Edit profile") composeRule.awaitText("Appearance") composeRule.awaitText("Defaults for new uploads") } @Test fun theUploadSwitchesShowWhatTheServerHolds() { show() composeRule.awaitText("Include camera data") composeRule.awaitText("Publish EXIF alongside each photo.") composeRule.awaitText("Include location") } @Test fun theBackButtonAndTheProfileRowReportTheirTaps() { var back = 0 var edit = 0 show(onBack = { back += 1 }, onEditProfile = { edit += 1 }) composeRule.onNodeWithText("Edit profile").performClick() composeRule.onNodeWithContentDescriptionCompat("Back").performClick() assertEquals(1, edit) assertEquals(1, back) } @Test fun choosingAnAppearanceStoresIt() { show() composeRule.onNodeWithText("Dark").performScrollTo().performClick() assertEquals(Appearance.DARK, appearance.appearance.value) } @Test fun signingOutGoesThroughAConfirmationThatCanBeCancelled() { show() composeRule.onNodeWithText("Sign out").performScrollTo().performClick() composeRule.awaitText("Sign out?") composeRule.awaitText("You'll need to sign in again to use Grain.") composeRule.onNodeWithText("Cancel").performClick() assertTrue("Cancelling leaves the session alone", harness.auth.state.value.isAuthenticated) } @Test fun confirmingTheSignOutEndsTheSession() { show() composeRule.onNodeWithText("Sign out").performScrollTo().performClick() composeRule.onAllNodesWithTextCompat("Sign out")[1].performClick() composeRule.waitForIdle() assertFalse(harness.auth.state.value.isAuthenticated) } @Test fun deletingTheAccountGoesThroughItsOwnConfirmation() { show() composeRule.onNodeWithText("Delete account").performScrollTo().performClick() composeRule.awaitText("Delete account?") composeRule.onNodeWithText("Cancel").performClick() composeRule.waitForIdle() assertEquals(0, harness.server.countOf("social.grain.unspecced.deleteAccount")) } @Test fun theModerationRowsLeadToTheirLists() { var blocked = 0 var muted = 0 show(onOpenBlocked = { blocked += 1 }, onOpenMuted = { muted += 1 }) composeRule.onNodeWithText("Blocked users").performScrollTo().performClick() composeRule.onNodeWithText("Muted users").performScrollTo().performClick() assertEquals(1, blocked) assertEquals(1, muted) } } private fun androidx.compose.ui.test.junit4.ComposeTestRule.onNodeWithContentDescriptionCompat( value: String, ) = onNode(androidx.compose.ui.test.hasContentDescription(value)) private fun androidx.compose.ui.test.junit4.ComposeTestRule.onAllNodesWithTextCompat(value: String) = onAllNodes(androidx.compose.ui.test.hasText(value))