Skip to content

Commit 3f5c7a9

Browse files
committed
Migrate DateTimePicker widget with both date and time components
1 parent ff18e30 commit 3f5c7a9

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
@@ -57,16 +62,15 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment
5762
import com.google.android.fhir.datacapture.R
5863
import com.google.android.fhir.datacapture.extensions.localDate
5964
import com.google.android.fhir.datacapture.extensions.localDateTime
60-
import com.google.android.fhir.datacapture.test.utilities.clickIcon
6165
import com.google.android.fhir.datacapture.test.utilities.clickOnText
6266
import com.google.android.fhir.datacapture.validation.Invalid
6367
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
6468
import com.google.android.fhir.datacapture.validation.Valid
6569
import com.google.android.fhir.datacapture.views.compose.DATE_TEXT_INPUT_FIELD
6670
import com.google.android.fhir.datacapture.views.compose.EDIT_TEXT_FIELD_TEST_TAG
6771
import com.google.android.fhir.datacapture.views.compose.HANDLE_INPUT_DEBOUNCE_TIME
72+
import com.google.android.fhir.datacapture.views.compose.TIME_PICKER_INPUT_FIELD
6873
import com.google.android.material.progressindicator.LinearProgressIndicator
69-
import com.google.android.material.textfield.TextInputLayout
7074
import com.google.common.truth.Truth.assertThat
7175
import java.math.BigDecimal
7276
import java.time.LocalDate
@@ -232,57 +236,70 @@ class QuestionnaireUiEspressoTest {
232236
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
233237

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

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

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

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

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

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

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

271-
onView(withId(R.id.date_input_edit_text))
272-
.perform(ViewActions.click())
273-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
278+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
274279

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

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

288305
@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)