refactor: update EditDeviceProfileDialog using dynamic fields

This commit is contained in:
andrekir 2024-08-25 07:38:13 -03:00 committed by Andre K
parent 0b7718f8d5
commit 19e0f7d8b3
3 changed files with 144 additions and 71 deletions

View file

@ -0,0 +1,93 @@
package com.geeksville.mesh.compose
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.R
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EditDeviceProfileDialogTest {
@get:Rule
val composeTestRule = createComposeRule()
private fun getString(id: Int): String =
InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
private val title = "Export configuration"
private val deviceProfile = deviceProfile {
longName = "Long name"
shortName = "Short name"
channelUrl = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ"
}
private fun testEditDeviceProfileDialog(
onDismissRequest: () -> Unit = {},
onAddClick: (DeviceProfile) -> Unit = {},
) = composeTestRule.setContent {
EditDeviceProfileDialog(
title = title,
deviceProfile = deviceProfile,
onAddClick = onAddClick,
onDismissRequest = onDismissRequest,
)
}
@Test
fun testEditDeviceProfileDialog_showsDialogTitle() {
composeTestRule.apply {
testEditDeviceProfileDialog()
// Verify that the dialog title is displayed
onNodeWithText(title).assertIsDisplayed()
}
}
@Test
fun testEditDeviceProfileDialog_showsCancelAndSaveButtons() {
composeTestRule.apply {
testEditDeviceProfileDialog()
// Verify the "Cancel" and "Save" buttons are displayed
onNodeWithText(getString(R.string.cancel)).assertIsDisplayed()
onNodeWithText(getString(R.string.save)).assertIsDisplayed()
}
}
@Test
fun testEditDeviceProfileDialog_clickCancelButton() {
var onDismissClicked = false
composeTestRule.apply {
testEditDeviceProfileDialog(onDismissRequest = { onDismissClicked = true })
// Click the "Cancel" button
onNodeWithText(getString(R.string.cancel)).performClick()
}
// Verify onDismiss is called
Assert.assertTrue(onDismissClicked)
}
@Test
fun testEditDeviceProfileDialog_addChannels() {
var actualDeviceProfile: DeviceProfile? = null
composeTestRule.apply {
testEditDeviceProfileDialog(onAddClick = { actualDeviceProfile = it })
onNodeWithText(getString(R.string.save)).performClick()
}
// Verify onConfirm is called with the correct DeviceProfile
Assert.assertEquals(deviceProfile, actualDeviceProfile)
}
}

View file

@ -326,7 +326,8 @@ class RadioConfigViewModel @Inject constructor(
if (hasLongName() || hasShortName()) destNode.value?.user?.let {
val user = it.copy(
longName = if (hasLongName()) longName else it.longName,
shortName = if (hasShortName()) shortName else it.shortName
shortName = if (hasShortName()) shortName else it.shortName,
hwModel = MeshProtos.HardwareModel.UNSET,
)
if (it != user) setOwner(user.toProto())
}
@ -337,28 +338,22 @@ class RadioConfigViewModel @Inject constructor(
setResponseStateError(ex.customMessage)
}
if (hasConfig()) {
setConfig(config { device = config.device })
setConfig(config { position = config.position })
setConfig(config { power = config.power })
setConfig(config { network = config.network })
setConfig(config { display = config.display })
setConfig(config { lora = config.lora })
setConfig(config { bluetooth = config.bluetooth })
val descriptor = config.descriptorForType
config.allFields.forEach { (field, value) ->
val newConfig = ConfigProtos.Config.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
setConfig(newConfig)
}
}
if (hasModuleConfig()) moduleConfig.let {
setModuleConfig(moduleConfig { mqtt = it.mqtt })
setModuleConfig(moduleConfig { serial = it.serial })
setModuleConfig(moduleConfig { externalNotification = it.externalNotification })
setModuleConfig(moduleConfig { storeForward = it.storeForward })
setModuleConfig(moduleConfig { rangeTest = it.rangeTest })
setModuleConfig(moduleConfig { telemetry = it.telemetry })
setModuleConfig(moduleConfig { cannedMessage = it.cannedMessage })
setModuleConfig(moduleConfig { audio = it.audio })
setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware })
setModuleConfig(moduleConfig { neighborInfo = it.neighborInfo })
setModuleConfig(moduleConfig { ambientLighting = it.ambientLighting })
setModuleConfig(moduleConfig { detectionSensor = it.detectionSensor })
setModuleConfig(moduleConfig { paxcounter = it.paxcounter })
if (hasModuleConfig()) {
val descriptor = moduleConfig.descriptorForType
moduleConfig.allFields.forEach { (field, value) ->
val newConfig = ModuleConfigProtos.ModuleConfig.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
setModuleConfig(newConfig)
}
}
meshService?.commitEditSettings()
}

View file

@ -1,18 +1,19 @@
package com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -21,8 +22,9 @@ import com.geeksville.mesh.ClientOnlyProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.ui.components.SwitchPreference
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.protobuf.Descriptors
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditDeviceProfileDialog(
title: String,
@ -31,72 +33,55 @@ fun EditDeviceProfileDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
var longNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasLongName()) }
var shortNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasShortName()) }
var channelUrlInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasChannelUrl()) }
var configInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasConfig()) }
var moduleConfigInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasModuleConfig()) }
val state = remember {
val fields = deviceProfile.descriptorForType.fields
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>()
.apply { putAll(fields.associateWith(deviceProfile::hasField)) }
}
AlertDialog(
title = { Text(title) },
onDismissRequest = onDismissRequest,
text = {
AppCompatTheme {
Column(modifier.fillMaxWidth()) {
SwitchPreference(title = "longName",
checked = longNameInput,
enabled = deviceProfile.hasLongName(),
onCheckedChange = { longNameInput = it }
)
SwitchPreference(title = "shortName",
checked = shortNameInput,
enabled = deviceProfile.hasShortName(),
onCheckedChange = { shortNameInput = it }
)
SwitchPreference(title = "channelUrl",
checked = channelUrlInput,
enabled = deviceProfile.hasChannelUrl(),
onCheckedChange = { channelUrlInput = it }
)
SwitchPreference(title = "config",
checked = configInput,
enabled = deviceProfile.hasConfig(),
onCheckedChange = { configInput = it }
)
SwitchPreference(title = "moduleConfig",
checked = moduleConfigInput,
enabled = deviceProfile.hasModuleConfig(),
onCheckedChange = { moduleConfigInput = it }
Column(modifier.fillMaxWidth()) {
state.keys.sortedBy { it.number }.forEach { field ->
SwitchPreference(
title = field.name,
checked = state[field] == true,
enabled = deviceProfile.hasField(field),
onCheckedChange = { state[field] = it },
padding = PaddingValues(0.dp)
)
}
}
},
buttons = {
Row(
FlowRow(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
TextButton(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp)
.padding(horizontal = 24.dp)
.weight(1f),
onClick = onDismissRequest
) { Text(stringResource(R.string.cancel)) }
Button(
modifier = modifier
.fillMaxWidth()
.padding(end = 24.dp)
.padding(horizontal = 24.dp)
.weight(1f),
onClick = {
onAddClick(deviceProfile {
if (longNameInput) longName = deviceProfile.longName
if (shortNameInput) shortName = deviceProfile.shortName
if (channelUrlInput) channelUrl = deviceProfile.channelUrl
if (configInput) config = deviceProfile.config
if (moduleConfigInput) moduleConfig = deviceProfile.moduleConfig
})
val builder = ClientOnlyProtos.DeviceProfile.newBuilder()
deviceProfile.allFields.forEach { (field, value) ->
if (state[field] == true) {
builder.setField(field, value)
}
}
onAddClick(builder.build())
},
enabled = state.values.any { it },
) { Text(stringResource(R.string.save)) }
}
}