diff options
Diffstat (limited to 'tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java')
-rw-r--r-- | tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java new file mode 100644 index 0000000..5c7883c --- /dev/null +++ b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java @@ -0,0 +1,303 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.calendar; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static org.hamcrest.CoreMatchers.not; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.provider.CalendarContract; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; + +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.runner.lifecycle.ActivityLifecycleCallback; +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; +import androidx.test.runner.lifecycle.Stage; + +import com.android.car.calendar.common.Event; +import com.android.car.calendar.common.EventsLiveData; + +import com.google.common.collect.ImmutableList; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class CarCalendarUiTest { + private static final ZoneId BERLIN_ZONE_ID = ZoneId.of("Europe/Berlin"); + private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); + private static final Locale LOCALE = Locale.ENGLISH; + private static final ZonedDateTime CURRENT_DATE_TIME = + LocalDateTime.of(2019, 12, 10, 10, 10, 10, 500500).atZone(BERLIN_ZONE_ID); + private static final ZonedDateTime START_DATE_TIME = + CURRENT_DATE_TIME.truncatedTo(ChronoUnit.HOURS); + private static final String EVENT_TITLE = "the title"; + private static final String EVENT_LOCATION = "the location"; + private static final String EVENT_DESCRIPTION = "the description"; + private static final String CALENDAR_NAME = "the calendar name"; + private static final int CALENDAR_COLOR = 0xCAFEBABE; + private static final int EVENT_ATTENDEE_STATUS = + CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED; + + private final ActivityLifecycleCallback mLifecycleCallback = this::onActivityLifecycleChanged; + + @Rule + public final GrantPermissionRule permissionRule = + GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR); + + private List<Object[]> mTestEventRows; + + // These can be set in the test thread and read on the main thread. + private volatile CountDownLatch mEventChangesLatch; + + @Before + public void setUp() { + ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(mLifecycleCallback); + mTestEventRows = new ArrayList<>(); + } + + private void onActivityLifecycleChanged(Activity activity, Stage stage) { + if (stage.equals(Stage.PRE_ON_CREATE)) { + setActivityDependencies((CarCalendarActivity) activity); + } else if (stage.equals(Stage.CREATED)) { + observeEventsLiveData((CarCalendarActivity) activity); + } + } + + private void setActivityDependencies(CarCalendarActivity activity) { + Clock fixedTimeClock = Clock.fixed(CURRENT_DATE_TIME.toInstant(), BERLIN_ZONE_ID); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + MockContentResolver mockContentResolver = new MockContentResolver(context); + TestCalendarContentProvider testCalendarContentProvider = + new TestCalendarContentProvider(context); + mockContentResolver.addProvider(CalendarContract.AUTHORITY, testCalendarContentProvider); + activity.mDependencies = + new CarCalendarActivity.Dependencies(LOCALE, fixedTimeClock, mockContentResolver); + } + + private void observeEventsLiveData(CarCalendarActivity activity) { + CarCalendarViewModel carCalendarViewModel = + new ViewModelProvider(activity).get(CarCalendarViewModel.class); + EventsLiveData eventsLiveData = carCalendarViewModel.getEventsLiveData(); + mEventChangesLatch = new CountDownLatch(1); + + // Notifications occur on the main thread. + eventsLiveData.observeForever( + new Observer<ImmutableList<Event>>() { + // Ignore the first change event triggered on registration with default value. + boolean mIgnoredFirstChange; + + @Override + public void onChanged(ImmutableList<Event> events) { + if (mIgnoredFirstChange) { + // Signal that the events were changed and notified on main thread. + mEventChangesLatch.countDown(); + } + mIgnoredFirstChange = true; + } + }); + } + + @After + public void tearDown() { + ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(mLifecycleCallback); + } + + @Test + public void calendar_titleShows() { + try (ActivityScenario<CarCalendarActivity> ignored = + ActivityScenario.launch(CarCalendarActivity.class)) { + onView(withText(R.string.app_name)).check(matches(isDisplayed())); + } + } + + @Test + public void event_displayed() { + mTestEventRows.add(buildTestRow(START_DATE_TIME, 1, EVENT_TITLE, false)); + try (ActivityScenario<CarCalendarActivity> ignored = + ActivityScenario.launch(CarCalendarActivity.class)) { + waitForEventsChange(); + + // Wait for the UI to be updated with changed events. + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); + } + } + + @Test + public void singleAllDayEvent_notCollapsed() { + // All day events are stored in UTC time. + ZonedDateTime utcDayStartTime = + START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS); + + mTestEventRows.add(buildTestRow(utcDayStartTime, 24, EVENT_TITLE, true)); + + try (ActivityScenario<CarCalendarActivity> ignored = + ActivityScenario.launch(CarCalendarActivity.class)) { + waitForEventsChange(); + + // Wait for the UI to be updated with changed events. + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // A single all-day event should not be collapsible. + onView(withId(R.id.expand_collapse_icon)).check(doesNotExist()); + onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); + } + } + + @Test + public void multipleAllDayEvents_collapsed() { + mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE)); + mTestEventRows.add(buildTestRowAllDay("Another all day event")); + + try (ActivityScenario<CarCalendarActivity> ignored = + ActivityScenario.launch(CarCalendarActivity.class)) { + waitForEventsChange(); + + // Wait for the UI to be updated with changed events. + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // Multiple all-day events should be collapsed. + onView(withId(R.id.expand_collapse_icon)).check(matches(isDisplayed())); + onView(withText(EVENT_TITLE)).check(matches(not(isDisplayed()))); + } + } + + @Test + public void multipleAllDayEvents_expands() { + mTestEventRows.add(buildTestRowAllDay(EVENT_TITLE)); + mTestEventRows.add(buildTestRowAllDay("Another all day event")); + + try (ActivityScenario<CarCalendarActivity> ignored = + ActivityScenario.launch(CarCalendarActivity.class)) { + waitForEventsChange(); + + // Wait for the UI to be updated with changed events. + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // Multiple all-day events should be collapsed. + onView(withId(R.id.expand_collapse_icon)).perform(click()); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + onView(withText(EVENT_TITLE)).check(matches(isDisplayed())); + } + } + + private void waitForEventsChange() { + try { + mEventChangesLatch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private class TestCalendarContentProvider extends MockContentProvider { + TestCalendarContentProvider(Context context) { + super(context); + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + Bundle queryArgs, + CancellationSignal cancellationSignal) { + if (uri.toString().startsWith(CalendarContract.Instances.CONTENT_URI.toString())) { + MatrixCursor cursor = + new MatrixCursor( + new String[] { + CalendarContract.Instances.TITLE, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.DESCRIPTION, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.SELF_ATTENDEE_STATUS, + CalendarContract.Instances.CALENDAR_COLOR, + CalendarContract.Instances.CALENDAR_DISPLAY_NAME, + }); + for (Object[] row : mTestEventRows) { + cursor.addRow(row); + } + return cursor; + } else if (uri.equals(CalendarContract.Calendars.CONTENT_URI)) { + MatrixCursor cursor = new MatrixCursor(new String[] {" Test name"}); + cursor.addRow(new String[] {"Test value"}); + return cursor; + } + throw new IllegalStateException("Unexpected query uri " + uri); + } + } + + private Object[] buildTestRowAllDay(String title) { + // All day events are stored in UTC time. + ZonedDateTime utcDayStartTime = + START_DATE_TIME.withZoneSameInstant(UTC_ZONE_ID).truncatedTo(ChronoUnit.DAYS); + return buildTestRow(utcDayStartTime, 24, title, true); + } + + private static Object[] buildTestRow( + ZonedDateTime startDateTime, int eventDurationHours, String title, boolean allDay) { + return new Object[] { + title, + allDay ? 1 : 0, + startDateTime.toInstant().toEpochMilli(), + startDateTime.plusHours(eventDurationHours).toInstant().toEpochMilli(), + EVENT_DESCRIPTION, + EVENT_LOCATION, + EVENT_ATTENDEE_STATUS, + CALENDAR_COLOR, + CALENDAR_NAME + }; + } +} |