aboutsummaryrefslogtreecommitdiff
path: root/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java')
-rw-r--r--tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java303
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
+ };
+ }
+}