Skip to content

Commit 3979cda

Browse files
committed
Merge branch 'create-ui-for-custom-list-droid-654'
2 parents 461e29d + 1ff2611 commit 3979cda

File tree

124 files changed

+6128
-530
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+6128
-530
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th
3232
- Add toggle for enabling or disabling split tunneling.
3333
- Add auto connect and lockdown mode guide on platforms that has system vpn settings.
3434
- Add 3D map to Connect screen.
35+
- Add the ability to create and manage custom lists of relays.
3536

3637
### Changed
3738
- Change default obfuscation setting to `auto`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package net.mullvad.mullvadvpn
2+
3+
import androidx.compose.ui.test.ExperimentalTestApi
4+
import androidx.compose.ui.test.SemanticsNodeInteraction
5+
import androidx.compose.ui.test.invokeGlobalAssertions
6+
import androidx.compose.ui.test.longClick
7+
import androidx.compose.ui.test.performTouchInput
8+
9+
fun SemanticsNodeInteraction.performLongClick(): SemanticsNodeInteraction {
10+
@OptIn(ExperimentalTestApi::class) return this.invokeGlobalAssertions().performLongClickImpl()
11+
}
12+
13+
private fun SemanticsNodeInteraction.performLongClickImpl(): SemanticsNodeInteraction {
14+
return performTouchInput { longClick() }
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package net.mullvad.mullvadvpn.compose.data
2+
3+
import net.mullvad.mullvadvpn.model.Constraint
4+
import net.mullvad.mullvadvpn.model.PortRange
5+
import net.mullvad.mullvadvpn.model.RelayEndpointData
6+
import net.mullvad.mullvadvpn.model.RelayList
7+
import net.mullvad.mullvadvpn.model.RelayListCity
8+
import net.mullvad.mullvadvpn.model.RelayListCountry
9+
import net.mullvad.mullvadvpn.model.WireguardEndpointData
10+
import net.mullvad.mullvadvpn.model.WireguardRelayEndpointData
11+
import net.mullvad.mullvadvpn.relaylist.RelayItem
12+
import net.mullvad.mullvadvpn.relaylist.toRelayCountries
13+
14+
private val DUMMY_RELAY_1 =
15+
net.mullvad.mullvadvpn.model.Relay(
16+
hostname = "Relay host 1",
17+
active = true,
18+
endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData),
19+
owned = true,
20+
provider = "PROVIDER"
21+
)
22+
private val DUMMY_RELAY_2 =
23+
net.mullvad.mullvadvpn.model.Relay(
24+
hostname = "Relay host 2",
25+
active = true,
26+
endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData),
27+
owned = true,
28+
provider = "PROVIDER"
29+
)
30+
private val DUMMY_RELAY_CITY_1 = RelayListCity("Relay City 1", "RCi1", arrayListOf(DUMMY_RELAY_1))
31+
private val DUMMY_RELAY_CITY_2 = RelayListCity("Relay City 2", "RCi2", arrayListOf(DUMMY_RELAY_2))
32+
private val DUMMY_RELAY_COUNTRY_1 =
33+
RelayListCountry("Relay Country 1", "RCo1", arrayListOf(DUMMY_RELAY_CITY_1))
34+
private val DUMMY_RELAY_COUNTRY_2 =
35+
RelayListCountry("Relay Country 2", "RCo2", arrayListOf(DUMMY_RELAY_CITY_2))
36+
37+
private val DUMMY_WIREGUARD_PORT_RANGES = ArrayList<PortRange>()
38+
private val DUMMY_WIREGUARD_ENDPOINT_DATA = WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES)
39+
40+
val DUMMY_RELAY_COUNTRIES =
41+
RelayList(
42+
arrayListOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2),
43+
DUMMY_WIREGUARD_ENDPOINT_DATA,
44+
)
45+
.toRelayCountries(ownership = Constraint.Any(), providers = Constraint.Any())
46+
47+
val DUMMY_CUSTOM_LISTS =
48+
listOf(
49+
RelayItem.CustomList("First list", false, "1", locations = DUMMY_RELAY_COUNTRIES),
50+
RelayItem.CustomList("Empty list", expanded = false, "2", locations = emptyList())
51+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package net.mullvad.mullvadvpn.compose.dialog
2+
3+
import androidx.compose.ui.test.ExperimentalTestApi
4+
import androidx.compose.ui.test.onNodeWithTag
5+
import androidx.compose.ui.test.onNodeWithText
6+
import androidx.compose.ui.test.performClick
7+
import androidx.compose.ui.test.performTextInput
8+
import io.mockk.MockKAnnotations
9+
import io.mockk.mockk
10+
import io.mockk.verify
11+
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
12+
import net.mullvad.mullvadvpn.compose.setContentWithTheme
13+
import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState
14+
import net.mullvad.mullvadvpn.compose.test.CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG
15+
import net.mullvad.mullvadvpn.model.CustomListsError
16+
import org.junit.jupiter.api.BeforeEach
17+
import org.junit.jupiter.api.Test
18+
import org.junit.jupiter.api.extension.RegisterExtension
19+
20+
class CreateCustomListDialogTest {
21+
@OptIn(ExperimentalTestApi::class)
22+
@JvmField
23+
@RegisterExtension
24+
val composeExtension = createEdgeToEdgeComposeExtension()
25+
26+
@BeforeEach
27+
fun setup() {
28+
MockKAnnotations.init(this)
29+
}
30+
31+
@Test
32+
fun givenNoErrorShouldShowNoErrorMessage() =
33+
composeExtension.use {
34+
// Arrange
35+
val state = CreateCustomListUiState(error = null)
36+
setContentWithTheme { CreateCustomListDialog(state = state) }
37+
38+
// Assert
39+
onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist()
40+
onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist()
41+
}
42+
43+
@Test
44+
fun givenCustomListExistsShouldShowCustomListExitsErrorText() =
45+
composeExtension.use {
46+
// Arrange
47+
val state = CreateCustomListUiState(error = CustomListsError.CustomListExists)
48+
setContentWithTheme { CreateCustomListDialog(state = state) }
49+
50+
// Assert
51+
onNodeWithText(NAME_EXIST_ERROR_TEXT).assertExists()
52+
onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist()
53+
}
54+
55+
@Test
56+
fun givenOtherCustomListErrorShouldShowAnErrorOccurredErrorText() =
57+
composeExtension.use {
58+
// Arrange
59+
val state = CreateCustomListUiState(error = CustomListsError.OtherError)
60+
setContentWithTheme { CreateCustomListDialog(state = state) }
61+
62+
// Assert
63+
onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist()
64+
onNodeWithText(OTHER_ERROR_TEXT).assertExists()
65+
}
66+
67+
@Test
68+
fun whenCancelIsClickedShouldDismissDialog() =
69+
composeExtension.use {
70+
// Arrange
71+
val mockedOnDismiss: () -> Unit = mockk(relaxed = true)
72+
val state = CreateCustomListUiState()
73+
setContentWithTheme {
74+
CreateCustomListDialog(state = state, onDismiss = mockedOnDismiss)
75+
}
76+
77+
// Act
78+
onNodeWithText(CANCEL_BUTTON_TEXT).performClick()
79+
80+
// Assert
81+
verify { mockedOnDismiss.invoke() }
82+
}
83+
84+
@Test
85+
fun givenEmptyTextInputWhenSubmitIsClickedThenShouldNotCallOnCreate() =
86+
composeExtension.use {
87+
// Arrange
88+
val mockedCreateCustomList: (String) -> Unit = mockk(relaxed = true)
89+
val state = CreateCustomListUiState()
90+
setContentWithTheme {
91+
CreateCustomListDialog(state = state, createCustomList = mockedCreateCustomList)
92+
}
93+
94+
// Act
95+
onNodeWithText(CREATE_BUTTON_TEXT).performClick()
96+
97+
// Assert
98+
verify(exactly = 0) { mockedCreateCustomList.invoke(any()) }
99+
}
100+
101+
@Test
102+
fun givenValidTextInputWhenSubmitIsClickedThenShouldCallOnCreate() =
103+
composeExtension.use {
104+
// Arrange
105+
val mockedCreateCustomList: (String) -> Unit = mockk(relaxed = true)
106+
val inputText = "NEW LIST"
107+
val state = CreateCustomListUiState()
108+
setContentWithTheme {
109+
CreateCustomListDialog(state = state, createCustomList = mockedCreateCustomList)
110+
}
111+
112+
// Act
113+
onNodeWithTag(CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText)
114+
onNodeWithText(CREATE_BUTTON_TEXT).performClick()
115+
116+
// Assert
117+
verify { mockedCreateCustomList.invoke(inputText) }
118+
}
119+
120+
@Test
121+
fun whenInputIsChangedShouldCallOnInputChanged() =
122+
composeExtension.use {
123+
// Arrange
124+
val mockedOnInputChanged: () -> Unit = mockk(relaxed = true)
125+
val inputText = "NEW LIST"
126+
val state = CreateCustomListUiState()
127+
setContentWithTheme {
128+
CreateCustomListDialog(state = state, onInputChanged = mockedOnInputChanged)
129+
}
130+
131+
// Act
132+
onNodeWithTag(CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText)
133+
134+
// Assert
135+
verify { mockedOnInputChanged.invoke() }
136+
}
137+
138+
companion object {
139+
private const val NAME_EXIST_ERROR_TEXT = "Name is already taken."
140+
private const val OTHER_ERROR_TEXT = "An error occurred."
141+
private const val CANCEL_BUTTON_TEXT = "Cancel"
142+
private const val CREATE_BUTTON_TEXT = "Create"
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package net.mullvad.mullvadvpn.compose.dialog
2+
3+
import androidx.compose.ui.test.ExperimentalTestApi
4+
import androidx.compose.ui.test.onNodeWithText
5+
import androidx.compose.ui.test.performClick
6+
import io.mockk.MockKAnnotations
7+
import io.mockk.mockk
8+
import io.mockk.verify
9+
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
10+
import net.mullvad.mullvadvpn.compose.setContentWithTheme
11+
import org.junit.jupiter.api.BeforeEach
12+
import org.junit.jupiter.api.Test
13+
import org.junit.jupiter.api.extension.RegisterExtension
14+
15+
class DeleteCustomListConfirmationDialogTest {
16+
@OptIn(ExperimentalTestApi::class)
17+
@JvmField
18+
@RegisterExtension
19+
val composeExtension = createEdgeToEdgeComposeExtension()
20+
21+
@BeforeEach
22+
fun setup() {
23+
MockKAnnotations.init(this)
24+
}
25+
26+
@Test
27+
fun givenNameShouldShowDeleteNameTitle() =
28+
composeExtension.use {
29+
// Arrange
30+
val name = "List should be deleted"
31+
setContentWithTheme { DeleteCustomListConfirmationDialog(name = name) }
32+
33+
// Assert
34+
onNodeWithText(DELETE_TITLE.format(name)).assertExists()
35+
}
36+
37+
@Test
38+
fun whenDeleteIsClickedShouldCallOnDelete() =
39+
composeExtension.use {
40+
// Arrange
41+
val name = "List should be deleted"
42+
val mockedOnDelete: () -> Unit = mockk(relaxed = true)
43+
setContentWithTheme {
44+
DeleteCustomListConfirmationDialog(name = name, onDelete = mockedOnDelete)
45+
}
46+
47+
// Act
48+
onNodeWithText(DELETE_BUTTON_TEXT).performClick()
49+
50+
// Assert
51+
verify { mockedOnDelete.invoke() }
52+
}
53+
54+
@Test
55+
fun whenCancelIsClickedShouldCallOnBack() =
56+
composeExtension.use {
57+
// Arrange
58+
val name = "List should be deleted"
59+
val mockedOnBack: () -> Unit = mockk(relaxed = true)
60+
setContentWithTheme {
61+
DeleteCustomListConfirmationDialog(name = name, onBack = mockedOnBack)
62+
}
63+
64+
// Act
65+
onNodeWithText(CANCEL_BUTTON_TEXT).performClick()
66+
67+
// Assert
68+
verify { mockedOnBack.invoke() }
69+
}
70+
71+
companion object {
72+
private const val DELETE_TITLE = "Delete \"%s\"?"
73+
private const val CANCEL_BUTTON_TEXT = "Cancel"
74+
private const val DELETE_BUTTON_TEXT = "Delete"
75+
}
76+
}

0 commit comments

Comments
 (0)