8

Im trying to create simple UI test using Espresso to set a date to newly created item.

Project is using https://github.com/wdullaer/MaterialDateTimePicker, but it shows dialog fragment with complex UI and nothing to hold on to.

I would like to create custom ViewAction to set the date or time similar to PickerActions from Espresso.

Any suggestions how to do it?

piotrek1543
  • 19,130
  • 7
  • 81
  • 94
Lubos Horacek
  • 1,562
  • 10
  • 28

7 Answers7

7

In the end I've created ViewAction that is able to set the time too, but its messy, as you have to know classname of view in the dialog, to have something to match with Matcher.

  /**
     * Returns a {@link ViewAction} that sets a date on a {@link DatePicker}.
     */
    public static ViewAction setDate(final int year, final int monthOfYear, final int dayOfMonth) {

        return new ViewAction() {

            @Override
            public void perform(UiController uiController, View view) {
                final DayPickerView dayPickerView = (DayPickerView) view;

                try {
                    Field f = null; //NoSuchFieldException
                    f = DayPickerView.class.getDeclaredField("mController");
                    f.setAccessible(true);
                    DatePickerController controller = (DatePickerController) f.get(dayPickerView); //IllegalAccessException
                    controller.onDayOfMonthSelected(year, monthOfYear, dayOfMonth);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public String getDescription() {
                return "set date";
            }

            @SuppressWarnings("unchecked")
            @Override
            public Matcher<View> getConstraints() {
                return allOf(isAssignableFrom(DayPickerView.class), isDisplayed());
            }
        };

    }

    /**
     * Returns a {@link ViewAction} that sets a time on a {@link TimePicker}.
     */
    public static ViewAction setTime(final int hours, final int minutes) {

        return new ViewAction() {

            @Override
            public void perform(UiController uiController, View view) {
                final RadialPickerLayout timePicker = (RadialPickerLayout) view;

                timePicker.setTime(new Timepoint(hours, minutes, 0));
            }

            @Override
            public String getDescription() {
                return "set time";
            }

            @SuppressWarnings("unchecked")
            @Override
            public Matcher<View> getConstraints() {
                return allOf(isAssignableFrom(RadialPickerLayout.class), isDisplayed());
            }
        };

    }

And usage:

onView(isAssignableFrom(DayPickerView.class)).perform(MaterialPickerActions.setDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)));

onView(isAssignableFrom(RadialPickerLayout.class)).perform(MaterialPickerActions.setTime(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)));
Lubos Horacek
  • 1,562
  • 10
  • 28
  • In the same example, I have one doubt. If suppose DayPickerView implement DatePickerController instead of creating the field then we are not able to get the field DayPickerView.class.getDeclaredField("mController"); So what we need to change for this behaviour. – kalpana c Nov 30 '16 at 13:48
  • Then just cast it after checking instanceof – Lubos Horacek Dec 04 '16 at 19:58
5

I used the built-in TimePickerDialog.

Looks like this

fun setAlarmTime(){

    val calendar = Calendar.getInstance()

    onView(isAssignableFrom(TimePicker::class.java)).perform(
        PickerActions.setTime(
            calendar.get(Calendar.HOUR_OF_DAY),
            calendar.get(Calendar.MINUTE) + 2
        )
    )
    onView(withText("OK")).perform(click())
}

Firstly, you should open the TimePickerDialog, then use this code. The time will be the current time + 2 minute. After the setup, it clicks on the OK button.

Marci
  • 899
  • 10
  • 13
2

I'm pretty sure that is impossible to test it with Espresso. You may need to take this action another UI testing tool called uiatomator.

uiatomator, another great tools made by Google allows you to test your Android system functions like notifications and screen lock. You can use it along with Espresso test framework.

Please read: Espresso & UIAutomator - the perfect tandem

and this official uiautomator documentation, which you would find here.

Also as I've already found uiautomator has a special class for testing this UI element called DatePickerHelper.java.

Check also this article: Crushing Fragmentation using the Factory Design Pattern with UiAutomator Notice that in this article author performs uiatomator tests on DatePicker.

Hope it help

piotrek1543
  • 19,130
  • 7
  • 81
  • 94
  • The thing is, MaterielDateTimePicker is not assignable to androids DateTimePicker. Id does not have one single view, but complex view hierarchy in DialogFragment. If it was, I would use PickerActions from espresso. I'm trying to keep the test structure as simple as possible as I'm not gonna be the one to write new test. But this time dialog is pretty common feature in our apps. – Lubos Horacek Jan 08 '16 at 09:23
  • I haven't used already `DatePicker` and this `MaterialDateTimePicker`, but I think in that case `uiautomator` would be more useful – piotrek1543 Jan 08 '16 at 09:41
1

I wrote this view action for a similar library.

public static ViewAction setCalendarDatePicker(final DateTime dateTime){
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            Matcher standardConstraint = ViewMatchers.isDisplayingAtLeast(90);
            return standardConstraint;
        }

        @Override
        public String getDescription() {
            return "setCalendarDatePicker";
        }

        @Override
        public void perform(UiController uiController, View view) {
            MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(dateTime.getMillis());
            DayPickerView datePicker = (DayPickerView) view;
            SimpleMonthAdapter adapter = (SimpleMonthAdapter) datePicker.getAdapter();
            datePicker.goTo(day, false, true, true);
            adapter.onDayClick(null, day);
        }
    };

Pretty sure there is a room for improvement, but it works. Give it a shot.

Be_Negative
  • 4,892
  • 1
  • 30
  • 33
1

I invested sometime in https://github.com/wdullaer/MaterialDateTimePicker and came up with the following solution as per how to handle the day , month and year.

Click on Date :

I was not able to handle date with robotium or Expresso. You need to add uiautomator dependency in the gradle file. Add the following lines in gradle file

androidTestCompile 'com.android.support:support-annotations:23.4.0'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'

Then upgrade the min sdk to 18 in the project gradle file for UiAutomator to work.

Now you can run the following code to set the date

     if (Build.VERSION.SDK_INT >= 23) {
                UiDevice device = UiDevice.getInstance(getInstrumentation());

//25 is the index which will click on 26 or 24 you need to figure out which one will work for you
                UiObject allowPermissions = device.findObject(new UiSelector().index(25));
                if (allowPermissions.exists()) {
                    try {
                        allowPermissions.click();
                    } catch (UiObjectNotFoundException e) {
                        Log.e("ERROR",e.toString());
                    }
                }
            }

Click on Year :

onView(withId(R.id.date_picker_year)).perform(click());
//If you want to set it as 2018. 
onView(withText("2018")).perform(click());

Click on Month :

  //Check how many swipeUp or swipeDown's are 
    onView(withId(R.id.animator)).perform(swipeUp());
1

Here's a Kotlin translation of Lubos' answer, although only for setDate

import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import com.wdullaer.materialdatetimepicker.date.DatePickerController
import com.wdullaer.materialdatetimepicker.date.DayPickerView
import org.hamcrest.Matcher
import org.hamcrest.core.AllOf
import java.lang.reflect.Field

class CustomViewActions {
    companion object {
        fun setDate(year: Int, monthOfYear: Int, dayOfMonth: Int): ViewAction {
            return object: ViewAction {
                override fun getConstraints(): Matcher<View> {
                    return allOf(isAssignableFrom(DayPickerView::class.java), isDisplayed())
                }

                override fun getDescription(): String {
                    return "set date"
                }

                override fun perform(uiController: UiController?, view: View?) {
                    try {
                        var f: Field? = null;
                        f = DayPickerView::class.java.getDeclaredField("mController")
                        f.isAccessible = true
                        val controller: DatePickerController = f.get(view) as DatePickerController
                        controller.onDayOfMonthSelected(year, monthOfYear, dayOfMonth)
                    } catch (e: NoSuchFieldException) {
                        e.printStackTrace()
                    } catch (e: IllegalAccessException) {
                        e.printStackTrace()
                    }
                }

            }


        }
    }
}
onView(isAssignableFrom(DayPickerView::class.java)).perform(CustomViewActions.setDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)));

agoff
  • 5,818
  • 1
  • 7
  • 20
1

I checked the IDs of the views with the LayoutInspector in Android Studio. If they wont't change in future releases these are the IDs and how it worked for me:

public static void setDate(LocalDate date){
    onView(withTagValue((Matchers.is((Object) ("TOGGLE_BUTTON_TAG"))))).perform(click());
    onView(withId(com.google.android.material.R.id.mtrl_picker_text_input_date)).perform(replaceText(date.format(DateTimeFormatter.ofPattern("M/d/yy"))));
}

public static void setTime(LocalTime time){
    onView(withId(com.google.android.material.R.id.material_timepicker_mode_button)).perform(click());
    onView(withId(com.google.android.material.R.id.material_hour_text_input)).perform(click());
    onView(allOf(isDisplayed(), withClassName(is(AppCompatEditText.class.getName())))).perform(replaceText(time.format(DateTimeFormatter.ofPattern("hh"))));
    onView(withId(com.google.android.material.R.id.material_minute_text_input)).perform(click());
    onView(allOf(isDisplayed(), withClassName(is(AppCompatEditText.class.getName())))).perform(replaceText(time.format(DateTimeFormatter.ofPattern("mm"))));
}
KaRa
  • 59
  • 7
  • This is not for the library that is referenced in the original question, but it was incredibly useful for me in using Google's MaterialDatePicker component. Thank you! – Chantell Osejo Nov 01 '22 at 22:12