Skip to content

Commit 708c675

Browse files
committed
Migrate DateTimePicker widget with both date and time components
1 parent fac2844 commit 708c675

File tree

11 files changed

+598
-656
lines changed

11 files changed

+598
-656
lines changed

datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,26 @@ package com.google.android.fhir.datacapture.test
1919
import android.view.View
2020
import android.widget.FrameLayout
2121
import android.widget.TextView
22+
import androidx.compose.ui.semantics.Role
2223
import androidx.compose.ui.semantics.SemanticsProperties
2324
import androidx.compose.ui.test.SemanticsMatcher
2425
import androidx.compose.ui.test.assert
2526
import androidx.compose.ui.test.assertIsDisplayed
27+
import androidx.compose.ui.test.assertIsEnabled
2628
import androidx.compose.ui.test.assertIsNotEnabled
2729
import androidx.compose.ui.test.assertTextEquals
30+
import androidx.compose.ui.test.filterToOne
2831
import androidx.compose.ui.test.hasAnyAncestor
2932
import androidx.compose.ui.test.hasText
3033
import androidx.compose.ui.test.isDialog
3134
import androidx.compose.ui.test.junit4.createEmptyComposeRule
35+
import androidx.compose.ui.test.onChildren
3236
import androidx.compose.ui.test.onNodeWithContentDescription
3337
import androidx.compose.ui.test.onNodeWithTag
3438
import androidx.compose.ui.test.onNodeWithText
3539
import androidx.compose.ui.test.performClick
3640
import androidx.compose.ui.test.performTextInput
41+
import androidx.compose.ui.test.performTextReplacement
3742
import androidx.fragment.app.commitNow
3843
import androidx.recyclerview.widget.RecyclerView
3944
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -58,16 +63,15 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment
5863
import com.google.android.fhir.datacapture.R
5964
import com.google.android.fhir.datacapture.extensions.localDate
6065
import com.google.android.fhir.datacapture.extensions.localDateTime
61-
import com.google.android.fhir.datacapture.test.utilities.clickIcon
6266
import com.google.android.fhir.datacapture.test.utilities.clickOnText
6367
import com.google.android.fhir.datacapture.validation.Invalid
6468
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
6569
import com.google.android.fhir.datacapture.validation.Valid
6670
import com.google.android.fhir.datacapture.views.compose.DATE_TEXT_INPUT_FIELD
6771
import com.google.android.fhir.datacapture.views.compose.EDIT_TEXT_FIELD_TEST_TAG
6872
import com.google.android.fhir.datacapture.views.compose.HANDLE_INPUT_DEBOUNCE_TIME
73+
import com.google.android.fhir.datacapture.views.compose.TIME_PICKER_INPUT_FIELD
6974
import com.google.android.material.progressindicator.LinearProgressIndicator
70-
import com.google.android.material.textfield.TextInputLayout
7175
import com.google.common.truth.Truth.assertThat
7276
import java.math.BigDecimal
7377
import java.time.LocalDate
@@ -233,57 +237,70 @@ class QuestionnaireUiEspressoTest {
233237
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
234238

235239
// Add month and day. No need to add slashes as they are added automatically
236-
onView(withId(R.id.date_input_edit_text))
237-
.perform(ViewActions.click())
238-
.perform(ViewActions.typeTextIntoFocusedView("0105"))
240+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("0105")
239241

240-
onView(withId(R.id.date_input_layout)).check { view, _ ->
241-
val actualError = (view as TextInputLayout).error
242-
assertThat(actualError).isEqualTo("Date format needs to be mm/dd/yyyy (e.g. 01/31/2023)")
243-
}
244-
onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isFalse() }
242+
composeTestRule
243+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
244+
.assert(
245+
SemanticsMatcher.expectValue(
246+
SemanticsProperties.Error,
247+
"Date format needs to be mm/dd/yyyy (e.g. 01/31/2023)",
248+
),
249+
)
250+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).assertIsNotEnabled()
245251
}
246252

247253
@Test
248254
fun dateTimePicker_shouldEnableTimePickerWithCorrectDate_butNotSaveInQuestionnaireResponse() {
249255
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
250256

251-
onView(withId(R.id.date_input_edit_text))
252-
.perform(ViewActions.click())
253-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
254-
255-
onView(withId(R.id.date_input_layout)).check { view, _ ->
256-
val actualError = (view as TextInputLayout).error
257-
assertThat(actualError).isEqualTo(null)
258-
}
257+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
259258

260-
onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isTrue() }
259+
composeTestRule
260+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
261+
.assert(
262+
SemanticsMatcher.keyNotDefined(
263+
SemanticsProperties.Error,
264+
),
265+
)
266+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).assertIsEnabled()
261267

262-
runBlocking {
263-
assertThat(getQuestionnaireResponse().item.size).isEqualTo(1)
264-
assertThat(getQuestionnaireResponse().item.first().answer.size).isEqualTo(0)
265-
}
268+
val questionnaireResponse = runBlocking { getQuestionnaireResponse() }
269+
assertThat(questionnaireResponse.item.size).isEqualTo(1)
270+
assertThat(questionnaireResponse.item.first().answer.size).isEqualTo(1)
271+
val answer = questionnaireResponse.item.first().answer.first().valueDateTimeType
272+
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 0, 0))
266273
}
267274

268275
@Test
269276
fun dateTimePicker_shouldSetAnswerWhenDateAndTimeAreFilled() {
270277
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
271278

272-
onView(withId(R.id.date_input_edit_text))
273-
.perform(ViewActions.click())
274-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
279+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
275280

276-
onView(withId(R.id.time_input_layout)).perform(clickIcon(true))
277-
clickOnText("AM")
278-
clickOnText("6")
279-
clickOnText("10")
280-
clickOnText("OK")
281+
composeTestRule
282+
.onNodeWithTag(TIME_PICKER_INPUT_FIELD)
283+
.onChildren()
284+
.filterToOne(
285+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
286+
)
287+
.performClick()
281288

282-
runBlocking {
283-
val answer = getQuestionnaireResponse().item.first().answer.first().valueDateTimeType
284-
// check Locale
285-
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
286-
}
289+
composeTestRule.onNodeWithText("AM").performClick()
290+
composeTestRule.onNodeWithContentDescription("Select hour", substring = true).performClick()
291+
composeTestRule.onNodeWithContentDescription("6 o'clock", substring = true).performClick()
292+
293+
composeTestRule.onNodeWithContentDescription("Select minutes", substring = true).performClick()
294+
composeTestRule.onNodeWithContentDescription("10 minutes", substring = true).performClick()
295+
296+
composeTestRule.onNodeWithText("OK").performClick()
297+
// Synchronize
298+
composeTestRule.waitForIdle()
299+
300+
val questionnaireResponse = runBlocking { getQuestionnaireResponse() }
301+
val answer = questionnaireResponse.item.first().answer.first().valueDateTimeType
302+
// check Locale
303+
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
287304
}
288305

289306
@Test
Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,26 +16,30 @@
1616

1717
package com.google.android.fhir.datacapture.test.views
1818

19-
import android.view.View
2019
import android.widget.FrameLayout
21-
import androidx.test.espresso.Espresso.onView
22-
import androidx.test.espresso.action.ViewActions
23-
import androidx.test.espresso.assertion.ViewAssertions.matches
24-
import androidx.test.espresso.matcher.RootMatchers.isDialog
25-
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
26-
import androidx.test.espresso.matcher.ViewMatchers.withId
27-
import androidx.test.espresso.matcher.ViewMatchers.withText
20+
import androidx.compose.ui.semantics.Role
21+
import androidx.compose.ui.semantics.SemanticsProperties
22+
import androidx.compose.ui.test.SemanticsMatcher
23+
import androidx.compose.ui.test.assertIsDisplayed
24+
import androidx.compose.ui.test.filterToOne
25+
import androidx.compose.ui.test.hasAnyChild
26+
import androidx.compose.ui.test.hasContentDescription
27+
import androidx.compose.ui.test.isEditable
28+
import androidx.compose.ui.test.junit4.createEmptyComposeRule
29+
import androidx.compose.ui.test.onChildren
30+
import androidx.compose.ui.test.onNodeWithTag
31+
import androidx.compose.ui.test.onNodeWithText
32+
import androidx.compose.ui.test.performClick
2833
import androidx.test.ext.junit.rules.ActivityScenarioRule
2934
import androidx.test.ext.junit.runners.AndroidJUnit4
3035
import androidx.test.platform.app.InstrumentationRegistry
31-
import com.google.android.fhir.datacapture.R
3236
import com.google.android.fhir.datacapture.test.TestActivity
33-
import com.google.android.fhir.datacapture.test.utilities.clickIcon
3437
import com.google.android.fhir.datacapture.validation.NotValidated
3538
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
39+
import com.google.android.fhir.datacapture.views.compose.DATE_TEXT_INPUT_FIELD
40+
import com.google.android.fhir.datacapture.views.compose.TIME_PICKER_INPUT_FIELD
3641
import com.google.android.fhir.datacapture.views.factories.DateTimePickerViewHolderFactory
3742
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
38-
import org.hamcrest.CoreMatchers.allOf
3943
import org.hl7.fhir.r4.model.Questionnaire
4044
import org.hl7.fhir.r4.model.QuestionnaireResponse
4145
import org.junit.Before
@@ -51,14 +55,17 @@ class DateTimePickerViewHolderFactoryEspressoTest {
5155
var activityScenarioRule: ActivityScenarioRule<TestActivity> =
5256
ActivityScenarioRule(TestActivity::class.java)
5357

54-
private lateinit var parent: FrameLayout
58+
@get:Rule val composeTestRule = createEmptyComposeRule()
59+
5560
private lateinit var viewHolder: QuestionnaireItemViewHolder
5661

5762
@Before
5863
fun setup() {
59-
activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) }
60-
viewHolder = DateTimePickerViewHolderFactory.create(parent)
61-
setTestLayout(viewHolder.itemView)
64+
activityScenarioRule.scenario.onActivity { activity ->
65+
viewHolder = DateTimePickerViewHolderFactory.create(FrameLayout(activity))
66+
activity.setContentView(viewHolder.itemView)
67+
}
68+
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
6269
}
6370

6471
@Test
@@ -71,17 +78,29 @@ class DateTimePickerViewHolderFactoryEspressoTest {
7178
answersChangedCallback = { _, _, _, _ -> },
7279
)
7380

74-
runOnUI { viewHolder.bind(questionnaireItemView) }
75-
onView(withId(R.id.date_input_layout)).perform(clickIcon(true))
76-
onView(allOf(withText("OK")))
77-
.inRoot(isDialog())
78-
.check(matches(isDisplayed()))
79-
.perform(ViewActions.click())
80-
onView(withId(R.id.time_input_edit_text)).perform(ViewActions.click())
81-
// R.id.material_textinput_timepicker is the id for the text input in the time picker.
82-
onView(allOf(withId(com.google.android.material.R.id.material_textinput_timepicker)))
83-
.inRoot(isDialog())
84-
.check(matches(isDisplayed()))
81+
viewHolder.bind(questionnaireItemView)
82+
composeTestRule
83+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
84+
.onChildren()
85+
.filterToOne(
86+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
87+
)
88+
.performClick()
89+
composeTestRule.onNodeWithText("OK").performClick()
90+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).performClick()
91+
92+
composeTestRule
93+
.onNode(
94+
hasContentDescription("Switch to clock input", substring = true) and
95+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
96+
)
97+
.assertIsDisplayed()
98+
composeTestRule
99+
.onNode(hasContentDescription("for hour", substring = true) and isEditable())
100+
.assertIsDisplayed()
101+
composeTestRule
102+
.onNode(hasContentDescription("for minutes", substring = true) and isEditable())
103+
.assertExists()
85104
}
86105

87106
@Test
@@ -94,27 +113,34 @@ class DateTimePickerViewHolderFactoryEspressoTest {
94113
answersChangedCallback = { _, _, _, _ -> },
95114
)
96115

97-
runOnUI { viewHolder.bind(questionnaireItemView) }
98-
onView(withId(R.id.date_input_layout)).perform(clickIcon(true))
99-
onView(allOf(withText("OK")))
100-
.inRoot(isDialog())
101-
.check(matches(isDisplayed()))
102-
.perform(ViewActions.click())
103-
onView(withId(R.id.time_input_layout)).perform(clickIcon(true))
104-
// R.id.material_clock_face is the id for the clock input in the time picker.
105-
onView(allOf(withId(com.google.android.material.R.id.material_clock_face)))
106-
.inRoot(isDialog())
107-
.check(matches(isDisplayed()))
108-
}
109-
110-
/** Method to run code snippet on UI/main thread */
111-
private fun runOnUI(action: () -> Unit) {
112-
activityScenarioRule.scenario.onActivity { activity -> action() }
113-
}
116+
viewHolder.bind(questionnaireItemView)
117+
composeTestRule
118+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
119+
.onChildren()
120+
.filterToOne(
121+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
122+
)
123+
.performClick()
124+
composeTestRule.onNodeWithText("OK").performClick()
125+
composeTestRule
126+
.onNodeWithTag(TIME_PICKER_INPUT_FIELD)
127+
.onChildren()
128+
.filterToOne(
129+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
130+
)
131+
.performClick()
114132

115-
/** Method to set content view for test activity */
116-
private fun setTestLayout(view: View) {
117-
activityScenarioRule.scenario.onActivity { activity -> activity.setContentView(view) }
118-
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
133+
composeTestRule
134+
.onNode(
135+
hasContentDescription("Switch to text input", substring = true) and
136+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
137+
)
138+
.assertIsDisplayed()
139+
composeTestRule
140+
.onNode(
141+
hasAnyChild(hasContentDescription("12 o'clock", substring = true)) and
142+
SemanticsMatcher.keyIsDefined(SemanticsProperties.SelectableGroup),
143+
)
144+
.assertIsDisplayed()
119145
}
120146
}

0 commit comments

Comments
 (0)