malliaridis commented on code in PR #3521:
URL: https://github.com/apache/solr/pull/3521#discussion_r2321979502


##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }

Review Comment:
   I would likely want to add this to the component state, so that when the 
user clicks on it, we can perform some tasks later, like "silent" refresh in 
the background (if that makes sense at some point). So the more is stored in 
the component, the more we are in control of user interactions.
   
   But we may also leave it as it is for now, and only update it later.



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsNavBar.kt:
##########
@@ -43,38 +43,32 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
-import org.apache.solr.ui.components.configsets.data.ListConfigsets
+import org.apache.solr.ui.components.configsets.data.Configset
 import org.apache.solr.ui.generated.resources.Res
-import org.apache.solr.ui.generated.resources.configsets_files
 import org.apache.solr.ui.generated.resources.configsets_index_query
-import org.apache.solr.ui.generated.resources.configsets_overview
-import org.apache.solr.ui.generated.resources.configsets_request_handlers
-import org.apache.solr.ui.generated.resources.configsets_schema
 import org.apache.solr.ui.generated.resources.configsets_search_components
 import org.apache.solr.ui.generated.resources.configsets_update_configuration
+import org.apache.solr.ui.generated.resources.files
+import org.apache.solr.ui.generated.resources.overview
+import org.apache.solr.ui.generated.resources.schema
 import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
 import org.jetbrains.compose.resources.stringResource
 
+
 @Composable
-fun ConfigsetsNavBarComponent(
+fun ConfigsetsNavBar(
     selectedTab: ConfigsetsTab,
     selectTab: (ConfigsetsTab) -> Unit,
-    selectedConfigSet: String,
-    selectConfigset: (String) -> Unit,
-    availableConfigsets: ListConfigsets,
+    availableConfigsets: List<Configset>,
     modifier: Modifier = Modifier,
-    content: @Composable (tab: ConfigsetsTab, configset: String) -> Unit = { 
tab, _ ->
-        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 
Text(stringResource(tabLabelRes(tab))) }
-    },
 ) {
-    if (availableConfigsets.names.isEmpty()) {
+    if (availableConfigsets.isEmpty()) {
         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
             Text("No configsets available")
         }
         return
     }

Review Comment:
   I would normally move this logic to the parent (`ConfigsetsContent`), so 
that the NavBar does not have any reference to information it doesnt use. 
Render the `ConfigsetsNavBar` only if the availableConfigsets is not empty (in 
parent), otherwise render this box. And you may still add to the 
`ConfigsetComponent.Model` a state value like `hasConfigsets: boolean` that you 
can directly use (and populate during mapping of state to model), so that you 
don't need to run a function on a collection in the UI (even if it is a cheap 
operation here).
   
   Most important though is that I would not hide the navigation elements 
(UI/UX). Navigation elements in general should stay where they are to maintain 
recognizability (users should see the screen and directly know where they are 
by the static elements like navigation options). They should also know what 
they can do here, even for a fresh installation. Most you could do (and I would 
not even do that) is to disable the navigation options. But instead, I would 
support in each section the case where no configset is selected yet, so that 
user can still navigate through the tabs and see what the content is, before 
actually taking any actions or loading any content.
   
   When no configset is selected, the tab may display the sections with small 
hints (like "Here you can manage the configsets files. Select a configset to 
get started." for the Files tab). But that is out of scope of this PR.



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import 
org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }
+    }
+
+    @Test
+    fun selectItem_updatesSelection_andClosesMenu() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(
+            names = listOf("gettingstarted", "techproducts"),
+            selected = "gettingstarted",
+        )
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Open
+        onNodeWithText("gettingstarted").assertExists().performClick()
+
+        // Select another
+        onNodeWithText("techproducts").assertExists().performClick()
+
+        // State updated + menu closed
+        runOnIdle {
+            assertEquals("techproducts", comp.model.value.selectedConfigset)
+            assertFalse(comp.model.value.expanded)
+        }
+    }
+
+    @Test
+    fun testSubsectionSwitch() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(names = listOf("basic_configs"))
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Click using the text node
+        onNodeWithText("Schema").assertExists().performClick()
+
+        runOnIdle { assertEquals(ConfigsetsTab.Schema, 
comp.model.value.selectedTab) }
+    }
+}
+
+private object DummyOverviewComponent : ConfigsetsOverviewComponent
+
+class TestConfigsetsComponent(
+    names: List<String>,
+    selected: String = "",
+    expanded: Boolean = false,
+) : ConfigsetsComponent {
+
+    private val _model = MutableStateFlow(
+        ConfigsetsComponent.Model(
+            selectedTab = ConfigsetsTab.Overview,
+            configsets = names.map(::Configset),
+            selectedConfigset = selected,
+            expanded = expanded,
+        ),
+    )
+    override val model: StateFlow<ConfigsetsComponent.Model> = _model

Review Comment:
   You may simplify your test component and your access in tests to your model 
values by not using a mutable state flow, but rather static fields where you 
can directly access. See for example 
[TestBasicAuthComponent](https://github.com/apache/solr/blob/main/solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/auth/TestBasicAuthComponent.kt).
 Of course the way you did it isn't wrong or bad, it is just less readable when 
accessing a field with `comp.model.value.selectedTab` instead of just 
`comp.selectedTab`. And you are close to start writing components that you have 
to test again due to the logic behind setting a field value and the 
relationship between fields. Imagine the case where you have `isValidEmail: 
Boolean` and `email: String` as state in your component. What would be the 
logic in your test component?



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import 
org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }

Review Comment:
   To keep it simple with less nesting, prefer `advanceUntilIdle()`



##########
solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsDropdown.kt:
##########
@@ -0,0 +1,64 @@
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.apache.solr.ui.components.configsets.data.Configset
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfigsetsDropdown(
+    selectedConfigSet: String,
+    selectConfigset: (String) -> Unit,
+    availableConfigsets: List<Configset>,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember { mutableStateOf(false) }
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = 16.dp, vertical = 8.dp),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        ExposedDropdownMenuBox(
+            expanded = expanded,
+            onExpandedChange = { expanded = !expanded },
+            modifier = Modifier.widthIn(min = 256.dp).weight(1f),
+        ) {
+            OutlinedTextField(
+                value = selectedConfigSet,
+                onValueChange = {},
+                readOnly = true,
+                label = { Text("Configset") },

Review Comment:
   I believe there are still some values that can be localized?



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import 
org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()
+
+        // Field click shouldn’t open the menu (anchor is disabled in our impl)
+        onNodeWithText("No configsets available").performClick()
+        runOnIdle { assertFalse(comp.model.value.expanded) }
+    }
+
+    @Test
+    fun selectItem_updatesSelection_andClosesMenu() = runComposeUiTest {

Review Comment:
   You are mixing here UI test with integration test of your test component 
(instead of your actual component). It is hard to explain this, but let me try 
to break it down anyway.
   
   The statement "select item updates selection and closes menu" is a test that 
tests the behavior of your test component, and not testing the UI itself. Pure 
UI tests (that use the test component) would be:
   - WHEN dropdown clicked THEN setMenuExpanded is called
   - WHEN dropdown item clicked THEN onSelectConfigset called
   
   Possible integration tests (tests that use the actual implementation of 
ConfigsetsComponent) would be:
   - GIVEN expanded is true AND some configsets WHEN configset selected THEN 
dropdown collapses
   - GIVEN expanded is true AND some configsets WHEN configset selected THEN 
selectedConfigset changes
   - GIVEN expanded is false AND some configsets WHEN expand menu THEN expanded 
is true
   - GIVEN expanded is false AND no configsets WHEN expand menu THEN expanded 
is false
   
   Btw. the interface function `setMenuExpanded` could be renamed to 
`onExpandMenu` for better readability, both in tests and in code.
   
   I believe with these test examples it is a bit clearer where to draw the 
lines between simple UI tests, that test composable functions with test 
components, and actual integration tests, that use actual implementations. One 
of the root causes why it is easy to confuse these is that we are providing 
both business logic and UI in the same package. If we would use different 
packages, it would be easier to distinguish them. business logic does not know 
UI composables, and UI composables do not know implementation details of 
business logic. We may consider splitting the UI module into two modules in the 
future (if that makes sense to have two modules for the UI).



##########
solr/ui/src/commonTest/kotlin/org/apache/solr/ui/views/configsets/ConfigsetsContentTest.kt:
##########
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.ui.views.configsets
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.apache.solr.ui.components.configsets.ConfigsetsComponent
+import org.apache.solr.ui.components.configsets.data.Configset
+import 
org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
+import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
+
+@OptIn(ExperimentalTestApi::class)
+class ConfigsetsContentTest {
+
+    @Test
+    fun emptyList_showsPlaceholder() = runComposeUiTest {
+        val comp = TestConfigsetsComponent(emptyList())
+
+        setContent { ConfigsetsContent(component = comp) }
+
+        // Placeholder text from the TextField
+        onNodeWithText("No configsets available").assertExists()

Review Comment:
   When we later introduce localization, these tests may fail becasue the text 
may be localized during tests and not found via `onNodeWithText`. Therefore, it 
is recommended to use `Modifier.testTag(...)` in the composables to check if 
specific composables are present.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to