diff options
| author | Albin <albin@mullvad.net> | 2023-03-22 17:46:34 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2023-03-22 17:46:34 +0100 |
| commit | 9dc003e9dfd2225c083c495c59263f1839b95e03 (patch) | |
| tree | ee30deb2e070db6a555da56a21fed829e77895b9 /android | |
| parent | 791dd555c6cbaaa317919b865dbb4523eafde4da (diff) | |
| parent | 128d188883d878dd84d3ace6dbbc6e553702a347 (diff) | |
| download | mullvadvpn-9dc003e9dfd2225c083c495c59263f1839b95e03.tar.xz mullvadvpn-9dc003e9dfd2225c083c495c59263f1839b95e03.zip | |
Merge branch 'add-automated-ui-tests-for-advanced-settings-droid-80'
Diffstat (limited to 'android')
4 files changed, 583 insertions, 24 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingsScreenTest.kt new file mode 100644 index 0000000000..1aaf119773 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingsScreenTest.kt @@ -0,0 +1,552 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +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 io.mockk.verifyAll +import net.mullvad.mullvadvpn.compose.state.AdvancedSettingsUiState +import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem +import net.mullvad.mullvadvpn.viewmodel.StagedDns +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AdvancedSettingsScreenTest { + @get:Rule val composeTestRule = createComposeRule() + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDefaultState() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen(uiState = AdvancedSettingsUiState.DefaultUiState()) + } + + // Assert + composeTestRule.apply { + onNodeWithText("WireGuard MTU").assertExists() + onNodeWithText("Default").assertExists() + onNodeWithText("Split tunneling").assertExists() + onNodeWithText("Use custom DNS server").assertExists() + onNodeWithText("Add a server").assertDoesNotExist() + } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuCustomValue() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.DefaultUiState(mtu = VALID_DUMMY_MTU_VALUE) + ) + } + + // Assert + composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuClick() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.DefaultUiState(), + onMtuCellClick = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText("WireGuard MTU").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogWithDefaultValue() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = EMPTY_STRING) + ) + } + + // Assert + composeTestRule.onNodeWithText(EMPTY_STRING).assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogWithEditValue() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = VALID_DUMMY_MTU_VALUE) + ) + } + + // Assert + composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogTextInput() { + // Arrange + val mockedInputHandler: (String) -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = EMPTY_STRING), + onMtuInputChange = mockedInputHandler + ) + } + + // Act + composeTestRule.onNodeWithText(EMPTY_STRING).performTextInput(VALID_DUMMY_MTU_VALUE) + + // Assert + verifyAll { mockedInputHandler.invoke(VALID_DUMMY_MTU_VALUE) } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogSubmitOfValidValue() { + // Arrange + val mockedSubmitHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = VALID_DUMMY_MTU_VALUE), + onSaveMtuClick = mockedSubmitHandler + ) + } + + // Act + composeTestRule.onNodeWithText("Submit").assertIsEnabled().performClick() + + // Assert + verify { mockedSubmitHandler.invoke() } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogSubmitButtonDisabledWhenInvalidInput() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = INVALID_DUMMY_MTU_VALUE) + ) + } + + // Assert + composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogResetClick() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = EMPTY_STRING), + onRestoreMtuClick = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText("Reset to default").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testMtuDialogCancelClick() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.MtuDialogUiState(mtuEditValue = EMPTY_STRING), + onCancelMtuDialogClicked = mockedClickHandler + ) + } + + // Assert + composeTestRule.onNodeWithText("Cancel").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testClickSplitTunneling() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.DefaultUiState(), + onSplitTunnelingNavigationClick = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText("Split tunneling").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testCustomDnsAddressesAndAddButtonVisibleWhenCustomDnsEnabled() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = true, + isAllowLanEnabled = false, + customDnsItems = + listOf( + CustomDnsItem(address = DUMMY_DNS_ADDRESS, false), + CustomDnsItem(address = DUMMY_DNS_ADDRESS_2, false), + CustomDnsItem(address = DUMMY_DNS_ADDRESS_3, false) + ) + ) + ) + } + + // Assert + composeTestRule.apply { + onNodeWithText(DUMMY_DNS_ADDRESS).assertExists() + onNodeWithText(DUMMY_DNS_ADDRESS_2).assertExists() + onNodeWithText(DUMMY_DNS_ADDRESS_3).assertExists() + onNodeWithText("Add a server").assertExists() + } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testCustomDnsAddressesAndAddButtonNotVisibleWhenCustomDnsDisabled() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = false, + customDnsItems = listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, false)) + ) + ) + } + + // Assert + composeTestRule.onNodeWithText(DUMMY_DNS_ADDRESS).assertDoesNotExist() + composeTestRule.onNodeWithText("Add a server").assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testLanWarningNotShownWhenLanTrafficEnabledAndLocalAddressIsUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = true, + isAllowLanEnabled = true, + customDnsItems = + listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, isLocal = true)) + ) + ) + } + + // Assert + composeTestRule.onNodeWithContentDescription(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testLanWarningNotShowedWhenLanTrafficDisabledAndLocalAddressIsNotUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = true, + isAllowLanEnabled = false, + customDnsItems = + listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, isLocal = false)) + ) + ) + } + + // Assert + composeTestRule.onNodeWithContentDescription(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testLanWarningNotShowedWhenLanTrafficEnabledAndLocalAddressIsNotUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = true, + isAllowLanEnabled = true, + customDnsItems = + listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, isLocal = false)) + ) + ) + } + + // Assert + composeTestRule.onNodeWithContentDescription(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testLanWarningShowedWhenAllowLanEnabledAndLocalDnsAddressIsUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DefaultUiState( + isCustomDnsEnabled = true, + isAllowLanEnabled = false, + customDnsItems = + listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, isLocal = true)) + ) + ) + } + + // Assert + composeTestRule.apply { + onNodeWithContentDescription(LOCAL_DNS_SERVER_WARNING).assertExists() + } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testClickAddDns() { + // Arrange + val mockedClickHandler: (Int?) -> Unit = mockk(relaxed = true) + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = AdvancedSettingsUiState.DefaultUiState(isCustomDnsEnabled = true), + onDnsClick = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText("Add a server").performClick() + + // Assert + verify { mockedClickHandler.invoke(null) } + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testShowDnsDialogForNewDnsServer() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false) + ) + ) + ) + } + + // Assert + composeTestRule.onNodeWithText("Add DNS server").assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testShowDnsDialogForUpdatingDnsServer() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.EditDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false), + index = 0 + ) + ) + ) + } + + // Assert + composeTestRule.onNodeWithText("Update DNS server").assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogLanWarningShownWhenLanTrafficDisabledAndLocalAddressUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = true), + validationResult = StagedDns.ValidationResult.Success + ), + isAllowLanEnabled = false + ) + ) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertExists() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndLocalAddressUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = true), + validationResult = StagedDns.ValidationResult.Success + ), + isAllowLanEnabled = true + ) + ) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndNonLocalAddressUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false), + validationResult = StagedDns.ValidationResult.Success + ), + isAllowLanEnabled = true + ) + ) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogLanWarningNotShownWhenLanTrafficDisabledAndNonLocalAddressUsed() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false), + validationResult = StagedDns.ValidationResult.Success + ), + isAllowLanEnabled = false + ) + ) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogSubmitButtonDisabledOnInvalidDnsAddress() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false), + validationResult = StagedDns.ValidationResult.InvalidAddress + ) + ) + ) + } + + // Assert + composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() + } + + @Test + @OptIn(ExperimentalMaterialApi::class) + fun testDnsDialogSubmitButtonDisabledOnDuplicateDnsAddress() { + // Arrange + composeTestRule.setContent { + AdvancedSettingScreen( + uiState = + AdvancedSettingsUiState.DnsDialogUiState( + stagedDns = + StagedDns.NewDns( + item = CustomDnsItem(DUMMY_DNS_ADDRESS, isLocal = false), + validationResult = StagedDns.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 EMPTY_STRING = "" + private const val VALID_DUMMY_MTU_VALUE = "1337" + private const val INVALID_DUMMY_MTU_VALUE = "1111" + private const val DUMMY_DNS_ADDRESS = "0.0.0.1" + private const val DUMMY_DNS_ADDRESS_2 = "0.0.0.2" + private const val DUMMY_DNS_ADDRESS_3 = "0.0.0.3" + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt index 3d23071e94..5020002bc7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.compose.component +import androidx.compose.foundation.background import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material.Scaffold @@ -49,7 +50,8 @@ fun ScaffoldWithTopBar( @Composable @OptIn(ExperimentalToolbarApi::class) fun CollapsableAwareToolbarScaffold( - modifier: Modifier, + backgroundColor: Color, + modifier: Modifier = Modifier, state: CollapsingToolbarScaffoldState, scrollStrategy: ScrollStrategy, isEnabledWhenCollapsable: Boolean = true, @@ -57,6 +59,10 @@ fun CollapsableAwareToolbarScaffold( toolbar: @Composable CollapsingToolbarScope.() -> Unit, body: @Composable CollapsingToolbarScaffoldScope.() -> Unit ) { + val systemUiController = rememberSystemUiController() + systemUiController.setStatusBarColor(backgroundColor) + systemUiController.setNavigationBarColor(backgroundColor) + var isCollapsable by remember { mutableStateOf(false) } LaunchedEffect(isCollapsable) { @@ -66,7 +72,7 @@ fun CollapsableAwareToolbarScaffold( } CollapsingToolbarScaffold( - modifier = modifier, + modifier = modifier.background(backgroundColor), state = state, scrollStrategy = scrollStrategy, enabled = isEnabledWhenCollapsable && isCollapsable, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt index ff0f471c88..bd115447d6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt @@ -73,19 +73,19 @@ private fun PreviewAdvancedSettings() { @Composable fun AdvancedSettingScreen( uiState: AdvancedSettingsUiState, - onMtuCellClick: () -> Unit, - onMtuInputChange: (String) -> Unit, - onSaveMtuClick: () -> Unit, - onRestoreMtuClick: () -> Unit, - onCancelMtuDialogClicked: () -> Unit, - onSplitTunnelingNavigationClick: () -> Unit, - onToggleDnsClick: (Boolean) -> Unit, - onDnsClick: (index: Int?) -> Unit, - onDnsInputChange: (String) -> Unit, - onSaveDnsClick: () -> Unit, - onRemoveDnsClick: () -> Unit, - onCancelDnsDialogClick: () -> Unit, - onBackClick: () -> Unit + onMtuCellClick: () -> Unit = {}, + onMtuInputChange: (String) -> Unit = {}, + onSaveMtuClick: () -> Unit = {}, + onRestoreMtuClick: () -> Unit = {}, + onCancelMtuDialogClicked: () -> Unit = {}, + onSplitTunnelingNavigationClick: () -> Unit = {}, + onToggleDnsClick: (Boolean) -> Unit = {}, + onDnsClick: (index: Int?) -> Unit = {}, + onDnsInputChange: (String) -> Unit = {}, + onSaveDnsClick: () -> Unit = {}, + onRemoveDnsClick: () -> Unit = {}, + onCancelDnsDialogClick: () -> Unit = {}, + onBackClick: () -> Unit = {} ) { val cellVerticalSpacing = dimensionResource(id = R.dimen.cell_label_vertical_padding) val cellHorizontalSpacing = dimensionResource(id = R.dimen.cell_left_padding) @@ -124,7 +124,8 @@ fun AdvancedSettingScreen( val progress = state.toolbarState.progress CollapsableAwareToolbarScaffold( - modifier = Modifier.background(MullvadDarkBlue).fillMaxSize(), + backgroundColor = MullvadDarkBlue, + modifier = Modifier.fillMaxSize(), state = state, scrollStrategy = ScrollStrategy.ExitUntilCollapsed, isEnabledWhenCollapsable = true, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt index ce554115d2..59c3551eb3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt @@ -17,18 +17,18 @@ sealed interface AdvancedSettingsUiState { ) : AdvancedSettingsUiState data class MtuDialogUiState( - override val mtu: String, - override val isCustomDnsEnabled: Boolean, - override val isAllowLanEnabled: Boolean, - override val customDnsItems: List<CustomDnsItem>, + override val mtu: String = "", + override val isCustomDnsEnabled: Boolean = false, + override val isAllowLanEnabled: Boolean = false, + override val customDnsItems: List<CustomDnsItem> = listOf(), val mtuEditValue: String ) : AdvancedSettingsUiState data class DnsDialogUiState( - override val mtu: String, - override val isCustomDnsEnabled: Boolean, - override val isAllowLanEnabled: Boolean, - override val customDnsItems: List<CustomDnsItem>, + override val mtu: String = "", + override val isCustomDnsEnabled: Boolean = false, + override val isAllowLanEnabled: Boolean = false, + override val customDnsItems: List<CustomDnsItem> = listOf(), val stagedDns: StagedDns, ) : AdvancedSettingsUiState } |
