Skip to content

Commit 198f938

Browse files
authored
Forms: Add theming support (#362)
1 parent b37b325 commit 198f938

File tree

13 files changed

+1690
-192
lines changed

13 files changed

+1690
-192
lines changed

toolkit/featureforms/api/featureforms.api

+233-1
Large diffs are not rendered by default.

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

+19
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.compose.ui.test.assert
2929
import androidx.compose.ui.test.onChildren
3030
import androidx.compose.ui.text.AnnotatedString
3131
import androidx.compose.ui.text.TextLayoutResult
32+
import androidx.compose.ui.text.TextStyle
3233

3334
fun SemanticsNodeInteraction.getAnnotatedTextString(): AnnotatedString {
3435
val textList = fetchSemanticsNode().config.first {
@@ -59,6 +60,24 @@ private fun isOfColor(color: Color): SemanticsMatcher = SemanticsMatcher(
5960
}
6061
}
6162

63+
fun SemanticsNodeInteraction.assertTextStyle(
64+
style: TextStyle
65+
): SemanticsNodeInteraction = assert(isOfTextStyle(style))
66+
67+
private fun isOfTextStyle(style: TextStyle): SemanticsMatcher = SemanticsMatcher(
68+
"${SemanticsProperties.Text.name} is of style '$style'"
69+
) {
70+
val textLayoutResults = mutableListOf<TextLayoutResult>()
71+
it.config.getOrNull(SemanticsActions.GetTextLayoutResult)
72+
?.action
73+
?.invoke(textLayoutResults)
74+
return@SemanticsMatcher if (textLayoutResults.isEmpty()) {
75+
false
76+
} else {
77+
textLayoutResults.first().layoutInput.style == style
78+
}
79+
}
80+
6281
private fun SemanticsNodeInteractionCollection.onChildWithText(value: String, recurse: Boolean = false): SemanticsNodeInteraction? {
6382
val count = fetchSemanticsNodes().count()
6483

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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 androidx.compose.ui.graphics.Color
20+
import androidx.compose.ui.test.assertIsDisplayed
21+
import androidx.compose.ui.test.junit4.createComposeRule
22+
import androidx.compose.ui.test.onNodeWithText
23+
import androidx.compose.ui.test.performClick
24+
import androidx.compose.ui.test.performImeAction
25+
import androidx.compose.ui.test.performTextInput
26+
import androidx.compose.ui.text.TextStyle
27+
import androidx.compose.ui.text.font.FontWeight
28+
import com.arcgismaps.ArcGISEnvironment
29+
import com.arcgismaps.data.ArcGISFeature
30+
import com.arcgismaps.data.QueryParameters
31+
import com.arcgismaps.mapping.ArcGISMap
32+
import com.arcgismaps.mapping.featureforms.FeatureForm
33+
import com.arcgismaps.mapping.featureforms.FieldFormElement
34+
import com.arcgismaps.mapping.featureforms.FormElement
35+
import com.arcgismaps.mapping.featureforms.GroupFormElement
36+
import com.arcgismaps.mapping.layers.FeatureLayer
37+
import com.arcgismaps.toolkit.featureforms.theme.FeatureFormColorScheme
38+
import com.arcgismaps.toolkit.featureforms.theme.FeatureFormDefaults
39+
import com.arcgismaps.toolkit.featureforms.theme.FeatureFormTypography
40+
import junit.framework.TestCase
41+
import kotlinx.coroutines.test.runTest
42+
import org.junit.BeforeClass
43+
import org.junit.Rule
44+
import org.junit.Test
45+
46+
class ThemingTests {
47+
48+
@get:Rule
49+
val composeTestRule = createComposeRule()
50+
51+
private fun getFormElementWithLabel(label: String): FormElement {
52+
return featureForm.elements.first {
53+
it.label == label
54+
}
55+
}
56+
57+
/**
58+
* Given a FeatureForm with a custom color scheme and typography for editable fields
59+
* When the FeatureForm is displayed
60+
* Then the custom color scheme and typography are applied to the editable form elements
61+
*/
62+
@Test
63+
fun testEditableFieldTheming() {
64+
var colorScheme: FeatureFormColorScheme
65+
var typography: FeatureFormTypography
66+
// create custom color scheme and typography and apply to the FeatureForm
67+
composeTestRule.setContent {
68+
colorScheme = FeatureFormDefaults.colorScheme(
69+
editableTextFieldColors = FeatureFormDefaults.editableTextFieldColors(
70+
focusedTextColor = Color.Red
71+
)
72+
)
73+
typography = FeatureFormDefaults.typography(
74+
editableTextFieldTypography = FeatureFormDefaults.editableTextFieldTypography(
75+
labelStyle = TextStyle(
76+
fontWeight = FontWeight.ExtraBold,
77+
color = Color.Green
78+
)
79+
)
80+
)
81+
FeatureForm(
82+
featureForm = featureForm,
83+
colorScheme = colorScheme,
84+
typography = typography
85+
)
86+
}
87+
val formElement = getFormElementWithLabel("Text Box") as FieldFormElement
88+
val label = composeTestRule.onNodeWithText(formElement.label, useUnmergedTree = true)
89+
label.assertIsDisplayed()
90+
label.assertTextStyle(
91+
TextStyle(
92+
fontWeight = FontWeight.ExtraBold,
93+
color = Color.Green
94+
)
95+
)
96+
val text =
97+
composeTestRule.onNodeWithText(formElement.formattedValue, useUnmergedTree = true)
98+
text.assertIsDisplayed()
99+
text.performClick()
100+
text.assertTextColor(Color.Red)
101+
}
102+
103+
/**
104+
* Given a FeatureForm with a custom color scheme that includes placeholder colors
105+
* When the FeatureForm is displayed and a form element is focused
106+
* Then the custom placeholder colors are applied to the form elements based on focus state
107+
*/
108+
@Test
109+
fun testPlaceHolderTransformation() {
110+
var colorScheme: FeatureFormColorScheme
111+
// create custom color scheme and typography and apply to the FeatureForm
112+
composeTestRule.setContent {
113+
colorScheme = FeatureFormDefaults.colorScheme(
114+
editableTextFieldColors = FeatureFormDefaults.editableTextFieldColors(
115+
unfocusedPlaceholderColor = Color.White,
116+
focusedPlaceholderColor = Color.Red,
117+
unfocusedTextColor = Color.Black,
118+
focusedTextColor = Color.Blue
119+
)
120+
)
121+
FeatureForm(
122+
featureForm = featureForm,
123+
colorScheme = colorScheme
124+
)
125+
}
126+
val formElement = getFormElementWithLabel("An empty field") as FieldFormElement
127+
val field = composeTestRule.onNodeWithText(formElement.label)
128+
val placeholder = composeTestRule.onNodeWithText(formElement.hint, useUnmergedTree = true)
129+
placeholder.assertIsDisplayed()
130+
// test unfocused placeholder color
131+
placeholder.assertTextColor(Color.White)
132+
field.performClick()
133+
// test focused placeholder color
134+
placeholder.assertTextColor(Color.Red)
135+
field.performTextInput("test")
136+
val text = composeTestRule.onNodeWithText("test", useUnmergedTree = true)
137+
text.assertIsDisplayed()
138+
// test focused text color
139+
text.assertTextColor(Color.Blue)
140+
field.performImeAction()
141+
// test unfocused text color
142+
text.assertTextColor(Color.Black)
143+
}
144+
145+
/**
146+
* Given a FeatureForm with a custom color scheme and typography for read only fields
147+
* When the FeatureForm is displayed
148+
* Then the custom color scheme and typography are applied to the read only form elements
149+
*/
150+
@Test
151+
fun testReadOnlyFieldTheming() {
152+
var colorScheme: FeatureFormColorScheme
153+
var typography: FeatureFormTypography
154+
composeTestRule.setContent {
155+
colorScheme = FeatureFormDefaults.colorScheme(
156+
readOnlyFieldColors = FeatureFormDefaults.readOnlyFieldColors(
157+
textColor = Color.Green
158+
)
159+
)
160+
typography = FeatureFormDefaults.typography(
161+
readOnlyFieldTypography = FeatureFormDefaults.readOnlyFieldTypography(
162+
labelStyle = TextStyle(
163+
fontWeight = FontWeight.ExtraBold,
164+
color = Color.Red
165+
)
166+
)
167+
)
168+
FeatureForm(
169+
featureForm = featureForm,
170+
colorScheme = colorScheme,
171+
typography = typography
172+
)
173+
}
174+
val formElement = getFormElementWithLabel("Name") as FieldFormElement
175+
val label = composeTestRule.onNodeWithText(formElement.label, useUnmergedTree = true)
176+
label.assertIsDisplayed()
177+
label.assertTextStyle(
178+
TextStyle(
179+
fontWeight = FontWeight.ExtraBold,
180+
color = Color.Red
181+
)
182+
)
183+
val text =
184+
composeTestRule.onNodeWithText(formElement.formattedValue, useUnmergedTree = true)
185+
text.assertIsDisplayed()
186+
text.assertTextColor(Color.Green)
187+
}
188+
189+
/**
190+
* Given a FeatureForm with a custom color scheme and typography for radio button fields
191+
* When the FeatureForm is displayed
192+
* Then the custom color scheme and typography are applied to the radio button form elements
193+
*/
194+
@Test
195+
fun testRadioButtonFieldTheming() {
196+
var colorScheme: FeatureFormColorScheme
197+
var typography: FeatureFormTypography
198+
composeTestRule.setContent {
199+
colorScheme = FeatureFormDefaults.colorScheme(
200+
radioButtonFieldColors = FeatureFormDefaults.radioButtonFieldColors(
201+
textColor = Color.Green
202+
)
203+
)
204+
typography = FeatureFormDefaults.typography(
205+
radioButtonFieldTypography = FeatureFormDefaults.radioButtonFieldTypography(
206+
labelStyle = TextStyle(
207+
fontWeight = FontWeight.ExtraBold,
208+
color = Color.Red
209+
)
210+
)
211+
)
212+
FeatureForm(
213+
featureForm = featureForm,
214+
colorScheme = colorScheme,
215+
typography = typography
216+
)
217+
}
218+
val formElement = getFormElementWithLabel("Radio Button") as FieldFormElement
219+
val label = composeTestRule.onNodeWithText(formElement.label, useUnmergedTree = true)
220+
label.assertIsDisplayed()
221+
label.assertTextStyle(
222+
TextStyle(
223+
fontWeight = FontWeight.ExtraBold,
224+
color = Color.Red
225+
)
226+
)
227+
val text =
228+
composeTestRule.onNodeWithText(formElement.formattedValue, useUnmergedTree = true)
229+
text.assertIsDisplayed()
230+
text.assertTextColor(Color.Green)
231+
}
232+
233+
/**
234+
* Given a FeatureForm with a custom color scheme and typography for group elements
235+
* When the FeatureForm is displayed
236+
* Then the custom color scheme and typography are applied to the group form elements
237+
*/
238+
@Test
239+
fun testGroupElementTheming() {
240+
var colorScheme: FeatureFormColorScheme
241+
var typography: FeatureFormTypography
242+
composeTestRule.setContent {
243+
colorScheme = FeatureFormDefaults.colorScheme(
244+
groupElementColors = FeatureFormDefaults.groupElementColors(
245+
supportingTextColor = Color.Green
246+
)
247+
)
248+
typography = FeatureFormDefaults.typography(
249+
groupElementTypography = FeatureFormDefaults.groupElementTypography(
250+
labelStyle = TextStyle(
251+
fontWeight = FontWeight.ExtraBold,
252+
color = Color.Blue
253+
)
254+
)
255+
)
256+
FeatureForm(
257+
featureForm = featureForm,
258+
colorScheme = colorScheme,
259+
typography = typography
260+
)
261+
}
262+
val groupElement = getFormElementWithLabel("Group One") as GroupFormElement
263+
val label = composeTestRule.onNodeWithText(groupElement.label, useUnmergedTree = true)
264+
label.assertIsDisplayed()
265+
label.assertTextStyle(
266+
TextStyle(
267+
fontWeight = FontWeight.ExtraBold,
268+
color = Color.Blue
269+
)
270+
)
271+
val supportingText = composeTestRule.onNodeWithText(groupElement.description, useUnmergedTree = true)
272+
supportingText.assertIsDisplayed()
273+
supportingText.assertTextColor(Color.Green)
274+
}
275+
276+
companion object {
277+
private lateinit var featureForm: FeatureForm
278+
279+
@BeforeClass
280+
@JvmStatic
281+
fun setupClass() = runTest {
282+
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler =
283+
FeatureFormsTestChallengeHandler(
284+
BuildConfig.webMapUser,
285+
BuildConfig.webMapPassword
286+
)
287+
val map =
288+
ArcGISMap("https://runtimecoretest.maps.arcgis.com/home/item.html?id=615e8fe546ef4d139fb9298515c2f40a")
289+
map.load().onFailure { TestCase.fail("failed to load webmap with ${it.message}") }
290+
val featureLayer = map.operationalLayers.first() as? FeatureLayer
291+
featureLayer?.let { layer ->
292+
layer.load().onFailure { TestCase.fail("failed to load layer with ${it.message}") }
293+
val featureFormDefinition = layer.featureFormDefinition!!
294+
val parameters = QueryParameters().also {
295+
it.objectIds.add(1L)
296+
it.maxFeatures = 1
297+
}
298+
layer.featureTable?.queryFeatures(parameters)?.onSuccess { featureQueryResult ->
299+
val feature = featureQueryResult.find {
300+
it is ArcGISFeature
301+
} as? ArcGISFeature
302+
if (feature == null) TestCase.fail("failed to fetch feature")
303+
feature?.load()?.onFailure {
304+
TestCase.fail("failed to load feature with ${it.message}")
305+
}
306+
featureForm = FeatureForm(feature!!, featureFormDefinition)
307+
featureForm.evaluateExpressions()
308+
}?.onFailure {
309+
TestCase.fail("failed to query features on layer's featuretable with ${it.message}")
310+
}
311+
}
312+
}
313+
}
314+
}

0 commit comments

Comments
 (0)