Skip to content

Commit 25bb605

Browse files
committed
Migrate DateTimePicker widget with both date and time components
1 parent 5e54875 commit 25bb605

File tree

11 files changed

+604
-671
lines changed

11 files changed

+604
-671
lines changed

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

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,31 @@ 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
28-
import androidx.compose.ui.test.junit4.createAndroidComposeRule
30+
import androidx.compose.ui.test.filterToOne
2931
import androidx.compose.ui.test.hasAnyAncestor
3032
import androidx.compose.ui.test.hasText
3133
import androidx.compose.ui.test.isDialog
3234
import androidx.compose.ui.test.junit4.createEmptyComposeRule
35+
import androidx.compose.ui.test.onChildren
3336
import androidx.compose.ui.test.onNodeWithContentDescription
3437
import androidx.compose.ui.test.onNodeWithTag
3538
import androidx.compose.ui.test.onNodeWithText
3639
import androidx.compose.ui.test.performClick
3740
import androidx.compose.ui.test.performTextInput
41+
import androidx.compose.ui.test.performTextReplacement
3842
import androidx.fragment.app.commitNow
3943
import androidx.test.espresso.Espresso.onView
4044
import androidx.test.espresso.action.ViewActions
4145
import androidx.test.espresso.assertion.ViewAssertions
4246
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
43-
import androidx.test.espresso.matcher.RootMatchers
44-
import androidx.test.espresso.contrib.RecyclerViewActions
4547
import androidx.test.espresso.matcher.ViewMatchers
4648
import androidx.test.espresso.matcher.ViewMatchers.withId
4749
import androidx.test.espresso.matcher.ViewMatchers.withText
@@ -55,16 +57,15 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment
5557
import com.google.android.fhir.datacapture.R
5658
import com.google.android.fhir.datacapture.extensions.localDate
5759
import com.google.android.fhir.datacapture.extensions.localDateTime
58-
import com.google.android.fhir.datacapture.test.utilities.clickIcon
5960
import com.google.android.fhir.datacapture.test.utilities.clickOnText
6061
import com.google.android.fhir.datacapture.validation.Invalid
6162
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
6263
import com.google.android.fhir.datacapture.validation.Valid
6364
import com.google.android.fhir.datacapture.views.compose.DATE_TEXT_INPUT_FIELD
6465
import com.google.android.fhir.datacapture.views.compose.EDIT_TEXT_FIELD_TEST_TAG
6566
import com.google.android.fhir.datacapture.views.compose.HANDLE_INPUT_DEBOUNCE_TIME
67+
import com.google.android.fhir.datacapture.views.compose.TIME_PICKER_INPUT_FIELD
6668
import com.google.android.material.progressindicator.LinearProgressIndicator
67-
import com.google.android.material.textfield.TextInputLayout
6869
import com.google.common.truth.Truth.assertThat
6970
import java.math.BigDecimal
7071
import java.time.LocalDate
@@ -231,57 +232,70 @@ class QuestionnaireUiEspressoTest {
231232
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
232233

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

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

245248
@Test
246249
fun dateTimePicker_shouldEnableTimePickerWithCorrectDate_butNotSaveInQuestionnaireResponse() {
247250
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
248251

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

258-
onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isTrue() }
254+
composeTestRule
255+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
256+
.assert(
257+
SemanticsMatcher.keyNotDefined(
258+
SemanticsProperties.Error,
259+
),
260+
)
261+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).assertIsEnabled()
259262

260-
runBlocking {
261-
assertThat(getQuestionnaireResponse().item.size).isEqualTo(1)
262-
assertThat(getQuestionnaireResponse().item.first().answer.size).isEqualTo(0)
263-
}
263+
val questionnaireResponse = runBlocking { getQuestionnaireResponse() }
264+
assertThat(questionnaireResponse.item.size).isEqualTo(1)
265+
assertThat(questionnaireResponse.item.first().answer.size).isEqualTo(1)
266+
val answer = questionnaireResponse.item.first().answer.first().valueDateTimeType
267+
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 0, 0))
264268
}
265269

266270
@Test
267271
fun dateTimePicker_shouldSetAnswerWhenDateAndTimeAreFilled() {
268272
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
269273

270-
onView(withId(R.id.date_input_edit_text))
271-
.perform(ViewActions.click())
272-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
274+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
275+
276+
composeTestRule
277+
.onNodeWithTag(TIME_PICKER_INPUT_FIELD)
278+
.onChildren()
279+
.filterToOne(
280+
SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button),
281+
)
282+
.performClick()
283+
284+
composeTestRule.onNodeWithText("AM").performClick()
285+
composeTestRule.onNodeWithContentDescription("Select hour", substring = true).performClick()
286+
composeTestRule.onNodeWithContentDescription("6 o'clock", substring = true).performClick()
273287

274-
onView(withId(R.id.time_input_layout)).perform(clickIcon(true))
275-
clickOnText("AM")
276-
clickOnText("6")
277-
clickOnText("10")
278-
clickOnText("OK")
288+
composeTestRule.onNodeWithContentDescription("Select minutes", substring = true).performClick()
289+
composeTestRule.onNodeWithContentDescription("10 minutes", substring = true).performClick()
279290

280-
runBlocking {
281-
val answer = getQuestionnaireResponse().item.first().answer.first().valueDateTimeType
282-
// check Locale
283-
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
284-
}
291+
composeTestRule.onNodeWithText("OK").performClick()
292+
// Synchronize
293+
composeTestRule.waitForIdle()
294+
295+
val questionnaireResponse = runBlocking { getQuestionnaireResponse() }
296+
val answer = questionnaireResponse.item.first().answer.first().valueDateTimeType
297+
// check Locale
298+
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
285299
}
286300

287301
@Test
@@ -649,8 +663,7 @@ class QuestionnaireUiEspressoTest {
649663
@Test
650664
fun test_repeated_group_is_added() {
651665
buildFragmentFromQuestionnaire("/component_repeated_group.json")
652-
onView(withId(R.id.add_item_to_repeated_group))
653-
.perform(ViewActions.click())
666+
onView(withId(R.id.add_item_to_repeated_group)).perform(ViewActions.click())
654667

655668
composeTestRule
656669
.onNodeWithTag(QuestionnaireFragment.QUESTIONNAIRE_EDIT_LIST)
@@ -660,8 +673,7 @@ class QuestionnaireUiEspressoTest {
660673
onView(withId(R.id.repeated_group_instance_header_title))
661674
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
662675

663-
onView(withText(R.string.delete))
664-
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
676+
onView(withText(R.string.delete)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
665677
}
666678

667679
@Test
@@ -695,11 +707,9 @@ class QuestionnaireUiEspressoTest {
695707
onView(withId(R.id.repeated_group_instance_header_title))
696708
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
697709

698-
onView(withText(R.string.delete))
699-
.perform(ViewActions.click())
710+
onView(withText(R.string.delete)).perform(ViewActions.click())
700711

701-
onView(withText(R.id.repeated_group_instance_header_title))
702-
.check(doesNotExist())
712+
onView(withText(R.id.repeated_group_instance_header_title)).check(doesNotExist())
703713
}
704714

705715
private fun buildFragmentFromQuestionnaire(
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)