Skip to content

Commit 76c76f5

Browse files
committed
Migrate DateTimePicker widget with both date and time components
# Conflicts: # datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt
1 parent 7b43f75 commit 76c76f5

File tree

11 files changed

+598
-657
lines changed

11 files changed

+598
-657
lines changed

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

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +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
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
@@ -55,16 +59,15 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment
5559
import com.google.android.fhir.datacapture.R
5660
import com.google.android.fhir.datacapture.extensions.localDate
5761
import com.google.android.fhir.datacapture.extensions.localDateTime
58-
import com.google.android.fhir.datacapture.test.utilities.clickIcon
5962
import com.google.android.fhir.datacapture.test.utilities.clickOnText
6063
import com.google.android.fhir.datacapture.validation.Invalid
6164
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
6265
import com.google.android.fhir.datacapture.validation.Valid
6366
import com.google.android.fhir.datacapture.views.compose.DATE_TEXT_INPUT_FIELD
6467
import com.google.android.fhir.datacapture.views.compose.EDIT_TEXT_FIELD_TEST_TAG
6568
import com.google.android.fhir.datacapture.views.compose.HANDLE_INPUT_DEBOUNCE_TIME
69+
import com.google.android.fhir.datacapture.views.compose.TIME_PICKER_INPUT_FIELD
6670
import com.google.android.material.progressindicator.LinearProgressIndicator
67-
import com.google.android.material.textfield.TextInputLayout
6871
import com.google.common.truth.Truth.assertThat
6972
import java.math.BigDecimal
7073
import java.time.LocalDate
@@ -231,57 +234,70 @@ class QuestionnaireUiEspressoTest {
231234
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
232235

233236
// 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"))
237+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("0105")
237238

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() }
239+
composeTestRule
240+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
241+
.assert(
242+
SemanticsMatcher.expectValue(
243+
SemanticsProperties.Error,
244+
"Date format needs to be mm/dd/yyyy (e.g. 01/31/2023)",
245+
),
246+
)
247+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).assertIsNotEnabled()
243248
}
244249

245250
@Test
246251
fun dateTimePicker_shouldEnableTimePickerWithCorrectDate_butNotSaveInQuestionnaireResponse() {
247252
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
248253

249-
onView(withId(R.id.date_input_edit_text))
250-
.perform(ViewActions.click())
251-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
254+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
252255

253-
onView(withId(R.id.date_input_layout)).check { view, _ ->
254-
val actualError = (view as TextInputLayout).error
255-
assertThat(actualError).isEqualTo(null)
256-
}
257-
258-
onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isTrue() }
256+
composeTestRule
257+
.onNodeWithTag(DATE_TEXT_INPUT_FIELD)
258+
.assert(
259+
SemanticsMatcher.keyNotDefined(
260+
SemanticsProperties.Error,
261+
),
262+
)
263+
composeTestRule.onNodeWithTag(TIME_PICKER_INPUT_FIELD).assertIsEnabled()
259264

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

266272
@Test
267273
fun dateTimePicker_shouldSetAnswerWhenDateAndTimeAreFilled() {
268274
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
269275

270-
onView(withId(R.id.date_input_edit_text))
271-
.perform(ViewActions.click())
272-
.perform(ViewActions.typeTextIntoFocusedView("01052005"))
276+
composeTestRule.onNodeWithTag(DATE_TEXT_INPUT_FIELD).performTextReplacement("01052005")
273277

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

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-
}
286+
composeTestRule.onNodeWithText("AM").performClick()
287+
composeTestRule.onNodeWithContentDescription("Select hour", substring = true).performClick()
288+
composeTestRule.onNodeWithContentDescription("6 o'clock", substring = true).performClick()
289+
290+
composeTestRule.onNodeWithContentDescription("Select minutes", substring = true).performClick()
291+
composeTestRule.onNodeWithContentDescription("10 minutes", substring = true).performClick()
292+
293+
composeTestRule.onNodeWithText("OK").performClick()
294+
// Synchronize
295+
composeTestRule.waitForIdle()
296+
297+
val questionnaireResponse = runBlocking { getQuestionnaireResponse() }
298+
val answer = questionnaireResponse.item.first().answer.first().valueDateTimeType
299+
// check Locale
300+
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
285301
}
286302

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