Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to compose navigation #5400

Merged
merged 7 commits into from
Dec 14, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Line wrap the file at 100 chars. Th
#### Linux
- Prevent fragmentation when multihop is enabled by setting a default route MTU.

### Changed
#### Android
- Migrated to Compose Navigation
- Allow for full rotation
- Improve animations between screens


## [android/2023.10-beta1] - 2023-12-11
### Fixed
Expand Down
8 changes: 6 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ plugins {
id(Dependencies.Plugin.playPublisherId)
id(Dependencies.Plugin.kotlinAndroidId)
id(Dependencies.Plugin.kotlinParcelizeId)
id(Dependencies.Plugin.ksp) version Versions.Plugin.ksp
}

val repoRootPath = rootProject.projectDir.absoluteFile.parentFile.absolutePath
val extraAssetsDirectory = "${project.buildDir}/extraAssets"
val defaultChangeLogAssetsDirectory = "$repoRootPath/android/src/main/play/release-notes/"
val defaultChangelogAssetsDirectory = "$repoRootPath/android/src/main/play/release-notes/"
val extraJniDirectory = "${project.buildDir}/extraJni"

val credentialsPath = "${rootProject.projectDir}/credentials"
Expand Down Expand Up @@ -111,7 +112,7 @@ android {
getByName("main") {
val changelogDir =
gradleLocalProperties(rootProject.projectDir)
.getOrDefault("OVERRIDE_CHANGELOG_DIR", defaultChangeLogAssetsDirectory)
.getOrDefault("OVERRIDE_CHANGELOG_DIR", defaultChangelogAssetsDirectory)

assets.srcDirs(extraAssetsDirectory, changelogDir)
jniLibs.srcDirs(extraJniDirectory)
Expand Down Expand Up @@ -337,6 +338,9 @@ dependencies {
implementation(Dependencies.Compose.uiController)
implementation(Dependencies.Compose.ui)
implementation(Dependencies.Compose.uiUtil)
implementation(Dependencies.Compose.destinations)
ksp(Dependencies.Compose.destinationsKsp)

implementation(Dependencies.jodaTime)
implementation(Dependencies.Koin.core)
implementation(Dependencies.Koin.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput
import io.mockk.MockKAnnotations
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.model.PortRange
import net.mullvad.mullvadvpn.onNodeWithTagAndText
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class CustomPortDialogTest {
@get:Rule val composeTestRule = createComposeRule()

@Before
fun setup() {
MockKAnnotations.init(this)
}

@SuppressLint("ComposableNaming")
@Composable
private fun testWireguardCustomPortDialog(
Pururun marked this conversation as resolved.
Show resolved Hide resolved
initialPort: Int? = null,
allowedPortRanges: List<PortRange> = emptyList(),
onSave: (Int?) -> Unit = { _ -> },
onDismiss: () -> Unit = {},
) {

WireguardCustomPortDialog(
initialPort = initialPort,
allowedPortRanges = allowedPortRanges,
onSave = onSave,
onDismiss = onDismiss
)
}

@Test
fun testShowWireguardCustomPortDialogInvalidInt() {
// Input a number to make sure that a too long number does not show and it does not crash
// the app

// Arrange
composeTestRule.setContentWithTheme { testWireguardCustomPortDialog() }

// Act
composeTestRule
.onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG)
.performTextInput(invalidCustomPort)

// Assert
composeTestRule
.onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, invalidCustomPort)
.assertDoesNotExist()
}

companion object {
const val invalidCustomPort = "21474836471"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewState
import org.junit.Rule
import org.junit.Test

class DnsDialogTest {
@get:Rule val composeTestRule = createComposeRule()

private val defaultState =
DnsDialogViewState(
ipAddress = "",
validationResult = DnsDialogViewState.ValidationResult.Success,
isLocal = false,
isAllowLanEnabled = false,
isNewEntry = true
)

@SuppressLint("ComposableNaming")
@Composable
private fun testDnsDialog(
Pururun marked this conversation as resolved.
Show resolved Hide resolved
state: DnsDialogViewState = defaultState,
onDnsInputChange: (String) -> Unit = { _ -> },
onSaveDnsClick: () -> Unit = {},
onRemoveDnsClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
DnsDialog(state, onDnsInputChange, onSaveDnsClick, onRemoveDnsClick, onDismiss)
}

@Test
fun testDnsDialogLanWarningShownWhenLanTrafficDisabledAndLocalAddressUsed() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(defaultState.copy(isAllowLanEnabled = false, isLocal = true))
}

// Assert
composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertExists()
}

@Test
fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndLocalAddressUsed() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(defaultState.copy(isAllowLanEnabled = true, isLocal = true))
}

// Assert
composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist()
}

@Test
fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndNonLocalAddressUsed() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(defaultState.copy(isAllowLanEnabled = true, isLocal = false))
}

// Assert
composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist()
}

@Test
fun testDnsDialogLanWarningNotShownWhenLanTrafficDisabledAndNonLocalAddressUsed() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(defaultState.copy(isAllowLanEnabled = false, isLocal = false))
}

// Assert
composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist()
}

@Test
fun testDnsDialogSubmitButtonDisabledOnInvalidDnsAddress() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(
defaultState.copy(
ipAddress = invalidIpAddress,
validationResult = DnsDialogViewState.ValidationResult.InvalidAddress,
)
)
}

// Assert
composeTestRule.onNodeWithText("Submit").assertIsNotEnabled()
}

@Test
fun testDnsDialogSubmitButtonDisabledOnDuplicateDnsAddress() {
// Arrange
composeTestRule.setContentWithTheme {
testDnsDialog(
defaultState.copy(
ipAddress = "192.168.0.1",
validationResult = DnsDialogViewState.ValidationResult.DuplicateAddress,
)
)
}

// Assert
composeTestRule.onNodeWithText("Submit").assertIsNotEnabled()
}

companion object {
private const val LOCAL_DNS_SERVER_WARNING =
"The local DNS server will not work unless you enable " +
"\"Local Network Sharing\" under Preferences."

private const val invalidIpAddress = "300.300.300.300"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class MtuDialogTest {
@get:Rule val composeTestRule = createComposeRule()

@Before
fun setup() {
MockKAnnotations.init(this)
}

@SuppressLint("ComposableNaming")
@Composable
private fun testMtuDialog(
Pururun marked this conversation as resolved.
Show resolved Hide resolved
mtuInitial: Int? = null,
onSaveMtu: (Int) -> Unit = { _ -> },
onResetMtu: () -> Unit = {},
onDismiss: () -> Unit = {},
) {
MtuDialog(
mtuInitial = mtuInitial,
onSaveMtu = onSaveMtu,
onResetMtu = onResetMtu,
onDismiss = onDismiss
)
}

@Test
fun testMtuDialogWithDefaultValue() {
// Arrange
composeTestRule.setContentWithTheme { testMtuDialog() }

// Assert
composeTestRule.onNodeWithText(EMPTY_STRING).assertExists()
}

@Test
fun testMtuDialogWithEditValue() {
// Arrange
composeTestRule.setContentWithTheme {
testMtuDialog(
mtuInitial = VALID_DUMMY_MTU_VALUE,
)
}

// Assert
composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists()
}

@Test
fun testMtuDialogTextInput() {
// Arrange
composeTestRule.setContentWithTheme {
testMtuDialog(
null,
)
}

// Act
composeTestRule
.onNodeWithText(EMPTY_STRING)
.performTextInput(VALID_DUMMY_MTU_VALUE.toString())

// Assert
composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists()
}

@Test
fun testMtuDialogSubmitOfValidValue() {
// Arrange
val mockedSubmitHandler: (Int) -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
testMtuDialog(
VALID_DUMMY_MTU_VALUE,
onSaveMtu = mockedSubmitHandler,
)
}

// Act
composeTestRule.onNodeWithText("Submit").assertIsEnabled().performClick()

// Assert
verify { mockedSubmitHandler.invoke(VALID_DUMMY_MTU_VALUE) }
}

@Test
fun testMtuDialogSubmitButtonDisabledWhenInvalidInput() {
// Arrange
composeTestRule.setContentWithTheme {
testMtuDialog(
INVALID_DUMMY_MTU_VALUE,
)
}

// Assert
composeTestRule.onNodeWithText("Submit").assertIsNotEnabled()
}

@Test
fun testMtuDialogResetClick() {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
testMtuDialog(
onResetMtu = mockedClickHandler,
)
}

// Act
composeTestRule.onNodeWithText("Reset to default").performClick()

// Assert
verify { mockedClickHandler.invoke() }
}

@Test
fun testMtuDialogCancelClick() {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
testMtuDialog(
onDismiss = mockedClickHandler,
)
}

// Assert
composeTestRule.onNodeWithText("Cancel").performClick()

// Assert
verify { mockedClickHandler.invoke() }
}

companion object {
private const val EMPTY_STRING = ""
private const val VALID_DUMMY_MTU_VALUE = 1337
private const val INVALID_DUMMY_MTU_VALUE = 1111
}
}
Loading
Loading