Skip to content

Commit f821830

Browse files
author
Soren Roth
committed
Merge remote-tracking branch 'origin/feature-branches/forms' into feature-branches/forms
2 parents 2091552 + 97cd35e commit f821830

File tree

4 files changed

+339
-2
lines changed

4 files changed

+339
-2
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2024 Esri
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.arcgismaps.toolkit.featureforms
18+
19+
import android.content.Context
20+
import androidx.compose.ui.platform.LocalContext
21+
import androidx.compose.ui.semantics.SemanticsProperties
22+
import androidx.compose.ui.test.SemanticsMatcher
23+
import androidx.compose.ui.test.assert
24+
import androidx.compose.ui.test.assertIsDisplayed
25+
import androidx.compose.ui.test.assertIsSelected
26+
import androidx.compose.ui.test.junit4.createComposeRule
27+
import androidx.compose.ui.test.onNodeWithText
28+
import androidx.compose.ui.test.performClick
29+
import com.arcgismaps.ArcGISEnvironment
30+
import com.arcgismaps.data.ArcGISFeature
31+
import com.arcgismaps.data.QueryParameters
32+
import com.arcgismaps.mapping.ArcGISMap
33+
import com.arcgismaps.mapping.featureforms.FeatureForm
34+
import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput
35+
import com.arcgismaps.mapping.layers.FeatureLayer
36+
import junit.framework.TestCase
37+
import kotlinx.coroutines.test.runTest
38+
import org.junit.Assert.fail
39+
import org.junit.Before
40+
import org.junit.BeforeClass
41+
import org.junit.Rule
42+
import org.junit.Test
43+
44+
class RadioButtonFieldTests {
45+
46+
@get:Rule
47+
val composeTestRule = createComposeRule()
48+
49+
private lateinit var context: Context
50+
51+
@Before
52+
fun setUp() {
53+
composeTestRule.setContent {
54+
context = LocalContext.current
55+
FeatureForm(featureForm = featureForm)
56+
}
57+
}
58+
59+
/**
60+
* Given a RadioFormInput with a pre-existing value and a no value label
61+
* When the FeatureForm is displayed
62+
* Then the RadioButtonField shows a no value option and indicates the pre-existing value is selected
63+
* And a new option is selected
64+
* Then the new selection is visible
65+
*/
66+
@Test
67+
fun testRadioButtonSelection() {
68+
val radioElement = featureForm.getFieldFormElementWithLabel("Radio Button Text")
69+
?: return fail("element not found")
70+
val input = radioElement.input as RadioButtonsFormInput
71+
// find the field with the the label
72+
val radioField = composeTestRule.onNodeWithText(radioElement.label)
73+
// assert it is displayed
74+
radioField.assertIsDisplayed()
75+
// assert the node has group selection indicating it is a radio button field
76+
radioField.assert(SemanticsMatcher.expectValue(SemanticsProperties.SelectableGroup, Unit))
77+
// assert "no value" option is visible
78+
radioField.onChildWithText(input.noValueLabel.ifEmpty { context.getString(R.string.no_value) }).assertExists()
79+
// check if the current value of the element is visible and selected
80+
radioField.onChildWithText(radioElement.formattedValue).assertIsSelected()
81+
// select the "dog" option
82+
radioField.onChildWithText("dog").performClick()
83+
// assert that the selected value has persisted
84+
assert(radioElement.formattedValue == "dog")
85+
// check if the current value of the element is visible and selected
86+
radioField.onChildWithText(radioElement.formattedValue).assertIsSelected()
87+
}
88+
89+
companion object {
90+
private lateinit var featureForm: FeatureForm
91+
92+
@BeforeClass
93+
@JvmStatic
94+
fun setupClass() = runTest {
95+
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler =
96+
FeatureFormsTestChallengeHandler(
97+
BuildConfig.webMapUser,
98+
BuildConfig.webMapPassword
99+
)
100+
val map =
101+
ArcGISMap("https://runtimecoretest.maps.arcgis.com/home/item.html?id=476e9b4180234961809485c8eff83d5d")
102+
map.load().onFailure { TestCase.fail("failed to load webmap with ${it.message}") }
103+
val featureLayer = map.operationalLayers.first() as? FeatureLayer
104+
featureLayer?.let { layer ->
105+
layer.load().onFailure { TestCase.fail("failed to load layer with ${it.message}") }
106+
val featureFormDefinition = layer.featureFormDefinition!!
107+
val parameters = QueryParameters().also {
108+
it.objectIds.add(1L)
109+
it.maxFeatures = 1
110+
}
111+
layer.featureTable?.queryFeatures(parameters)?.onSuccess { featureQueryResult ->
112+
val feature = featureQueryResult.find {
113+
it is ArcGISFeature
114+
} as? ArcGISFeature
115+
if (feature == null) TestCase.fail("failed to fetch feature")
116+
feature?.load()?.onFailure {
117+
TestCase.fail("failed to load feature with ${it.message}")
118+
}
119+
featureForm = FeatureForm(feature!!, featureFormDefinition)
120+
featureForm.evaluateExpressions()
121+
}?.onFailure {
122+
TestCase.fail("failed to query features on layer's featuretable with ${it.message}")
123+
}
124+
}
125+
}
126+
}
127+
}

toolkit/featureforms/src/androidTest/java/com/arcgismaps/toolkit/featureforms/SemanticsNodeUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private fun SemanticsNodeInteractionCollection.onChildWithText(value: String, re
7272
*
7373
* @param value the text string for which to search.
7474
* @param recurse if true will recurse through the whole semantic node hierarchy.
75-
* @throws AssertionError if the child with the content description does not exist.
75+
* @throws AssertionError if the child with the given text does not exist.
7676
*/
7777
@Throws(AssertionError::class)
7878
internal fun SemanticsNodeInteraction.onChildWithText(value: String, recurse: Boolean = false): SemanticsNodeInteraction {
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2024 Esri
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.arcgismaps.toolkit.featureforms
18+
19+
import android.content.Context
20+
import androidx.compose.ui.platform.LocalContext
21+
import androidx.compose.ui.test.assert
22+
import androidx.compose.ui.test.assertContentDescriptionContains
23+
import androidx.compose.ui.test.assertIsDisplayed
24+
import androidx.compose.ui.test.assertIsNotFocused
25+
import androidx.compose.ui.test.assertIsOff
26+
import androidx.compose.ui.test.assertIsOn
27+
import androidx.compose.ui.test.assertTextEquals
28+
import androidx.compose.ui.test.hasAnyChild
29+
import androidx.compose.ui.test.hasContentDescription
30+
import androidx.compose.ui.test.junit4.createComposeRule
31+
import androidx.compose.ui.test.onNodeWithText
32+
import androidx.compose.ui.test.performClick
33+
import com.arcgismaps.ArcGISEnvironment
34+
import com.arcgismaps.data.ArcGISFeature
35+
import com.arcgismaps.data.QueryParameters
36+
import com.arcgismaps.mapping.ArcGISMap
37+
import com.arcgismaps.mapping.featureforms.FeatureForm
38+
import com.arcgismaps.mapping.featureforms.FieldFormElement
39+
import com.arcgismaps.mapping.layers.FeatureLayer
40+
import junit.framework.TestCase
41+
import kotlinx.coroutines.test.runTest
42+
import org.junit.Assert.fail
43+
import org.junit.Before
44+
import org.junit.BeforeClass
45+
import org.junit.Rule
46+
import org.junit.Test
47+
48+
class SwitchFieldTests {
49+
50+
@get:Rule
51+
val composeTestRule = createComposeRule()
52+
53+
private lateinit var context: Context
54+
55+
@Before
56+
fun setUp() {
57+
composeTestRule.setContent {
58+
context = LocalContext.current
59+
FeatureForm(featureForm = featureForm)
60+
}
61+
}
62+
63+
/**
64+
* Test case 5.1:
65+
* Given a SwitchField type with a pre-existing "on" value
66+
* When the switch is tapped
67+
* Then the switch toggles to an "off" state.
68+
* https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/Forms/FormsTestDesign.md#test-case-51-test-switch-on
69+
*/
70+
@Test
71+
fun testSwitchIsOn() {
72+
// get the switch form element
73+
val switchElement = featureForm.getFieldFormElementWithLabel("switch integer")
74+
?: return fail("element not found")
75+
// find the field with the the label
76+
val switchField = composeTestRule.onNodeWithText(switchElement.label)
77+
// assert it is displayed and not focused
78+
switchField.assertIsDisplayed()
79+
switchField.assertIsNotFocused()
80+
// find the switch field
81+
val switch = switchField.onChildWithContentDescription("switch", recurse = true)
82+
switch.assertIsDisplayed()
83+
// assert that the switch is on
84+
switch.assertIsOn()
85+
// assert the value displayed is the current "on" value
86+
switchField.assertEditableTextEquals(switchElement.formattedValue)
87+
// tap on the switch
88+
switchField.performClick()
89+
// assert that the switch is on
90+
switch.assertIsOff()
91+
// assert the value displayed is the current "off" value
92+
switchField.assertEditableTextEquals(switchElement.formattedValue)
93+
}
94+
95+
/**
96+
* Test case 5.2:
97+
* Given a SwitchField type with a pre-existing "off" value
98+
* When the switch is tapped
99+
* Then the switch toggles to an "on" state.
100+
* https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/Forms/FormsTestDesign.md#test-case-52-test-switch-off
101+
*/
102+
@Test
103+
fun testSwitchIsOff() {
104+
// get the switch form element
105+
val switchElement = featureForm.getFieldFormElementWithLabel("switch string")
106+
?: return fail("element not found")
107+
// find the field with the the label
108+
val switchField = composeTestRule.onNodeWithText(switchElement.label)
109+
// assert it is displayed and not focused
110+
switchField.assertIsDisplayed()
111+
switchField.assertIsNotFocused()
112+
// find the switch control
113+
val switch = switchField.onChildWithContentDescription("switch", recurse = true)
114+
switch.assertIsDisplayed()
115+
// assert that the switch is off
116+
switch.assertIsOff()
117+
// assert the value displayed is the current "off" value
118+
switchField.assertEditableTextEquals(switchElement.formattedValue)
119+
// tap on the switch
120+
switchField.performClick()
121+
// assert that the switch is on
122+
switch.assertIsOn()
123+
// assert the value displayed is the current "on" value
124+
switchField.assertEditableTextEquals(switchElement.formattedValue)
125+
}
126+
127+
/**
128+
* Test case 5.3:
129+
* Given a FieldFormElement with a SwitchFormInput type and no pre-existing value
130+
* When the FeatureForm is displayed
131+
* Then the FieldFormElement is displayed as a ComboBox.
132+
* https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/Forms/FormsTestDesign.md#test-case-53-test-switch-with-no-value
133+
*/
134+
@Test
135+
fun testSwitchWithNoValue() {
136+
// get the switch form element
137+
val switchElement = featureForm.getFieldFormElementWithLabel("switch double")
138+
?: return fail("element not found")
139+
// find the field with the the label
140+
val comboBoxField = composeTestRule.onNodeWithText(switchElement.label)
141+
// assert it is displayed and not focused
142+
comboBoxField.assertIsDisplayed()
143+
comboBoxField.assertIsNotFocused()
144+
// assert that this field does not have any switch control
145+
comboBoxField.assert(!hasAnyChild(hasContentDescription("switch")))
146+
// assert a "no value" placeholder is visible
147+
comboBoxField.assertTextEquals(
148+
switchElement.label,
149+
context.getString(R.string.no_value)
150+
)
151+
// validate that the options icon is visible
152+
// since combo box fields have an icon and switch fields do not
153+
comboBoxField.assertContentDescriptionContains("field icon").assertIsDisplayed()
154+
}
155+
156+
companion object {
157+
private lateinit var featureForm: FeatureForm
158+
159+
@BeforeClass
160+
@JvmStatic
161+
fun setupClass() = runTest {
162+
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler =
163+
FeatureFormsTestChallengeHandler(
164+
BuildConfig.webMapUser,
165+
BuildConfig.webMapPassword
166+
)
167+
val map =
168+
ArcGISMap("https://runtimecoretest.maps.arcgis.com/home/item.html?id=ff98f13b32b349adb55da5528d9174dc")
169+
map.load().onFailure { TestCase.fail("failed to load webmap with ${it.message}") }
170+
val featureLayer = map.operationalLayers.first() as? FeatureLayer
171+
featureLayer?.let { layer ->
172+
layer.load().onFailure { TestCase.fail("failed to load layer with ${it.message}") }
173+
val featureFormDefinition = layer.featureFormDefinition!!
174+
val parameters = QueryParameters().also {
175+
it.objectIds.add(1L)
176+
it.maxFeatures = 1
177+
}
178+
layer.featureTable?.queryFeatures(parameters)?.onSuccess { featureQueryResult ->
179+
val feature = featureQueryResult.find {
180+
it is ArcGISFeature
181+
} as? ArcGISFeature
182+
if (feature == null) TestCase.fail("failed to fetch feature")
183+
feature?.load()?.onFailure {
184+
TestCase.fail("failed to load feature with ${it.message}")
185+
}
186+
featureForm = FeatureForm(feature!!, featureFormDefinition)
187+
featureForm.evaluateExpressions()
188+
}?.onFailure {
189+
TestCase.fail("failed to query features on layer's featuretable with ${it.message}")
190+
}
191+
}
192+
}
193+
}
194+
}
195+
196+
/**
197+
* Returns a [FieldFormElement] with the given [label] if it exists. Else a null is returned.
198+
*/
199+
internal fun FeatureForm.getFieldFormElementWithLabel(label: String): FieldFormElement? =
200+
elements.find {
201+
it is FieldFormElement && it.label == label
202+
} as? FieldFormElement
203+

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/RadioButtonField.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import androidx.compose.ui.Alignment
3737
import androidx.compose.ui.Modifier
3838
import androidx.compose.ui.res.stringResource
3939
import androidx.compose.ui.semantics.Role
40+
import androidx.compose.ui.semantics.contentDescription
41+
import androidx.compose.ui.semantics.semantics
4042
import androidx.compose.ui.tooling.preview.Preview
4143
import androidx.compose.ui.unit.dp
4244
import com.arcgismaps.mapping.featureforms.FormInputNoValueOption
@@ -104,6 +106,7 @@ private fun RadioButtonField(
104106
Column(
105107
modifier = modifier
106108
.fillMaxWidth()
109+
.semantics(mergeDescendants = true) {}
107110
.padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp),
108111
verticalArrangement = Arrangement.spacedBy(10.dp),
109112
horizontalAlignment = Alignment.Start
@@ -115,6 +118,7 @@ private fun RadioButtonField(
115118
label
116119
},
117120
style = RadioButtonFieldDefaults.labelTextStyle,
121+
modifier = Modifier.semantics { contentDescription = "label" }
118122
)
119123
Column(
120124
modifier = Modifier
@@ -140,7 +144,10 @@ private fun RadioButtonField(
140144
if (description.isNotEmpty()) {
141145
Text(
142146
text = description,
143-
style = RadioButtonFieldDefaults.supportingTextStyle
147+
style = RadioButtonFieldDefaults.supportingTextStyle,
148+
modifier = Modifier.semantics {
149+
contentDescription = "supporting text"
150+
}
144151
)
145152
}
146153
}

0 commit comments

Comments
 (0)