~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
190 lines · 6.0 KB Raw History
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(
            "social.grain.actor.getPreferences",
            """{"preferences":[{"${'$'}type":"social.grain.actor.defs#photoMetadataPref",""" +
                """"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.server.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))