// Copyright 2012 Square, Inc.
package com.squareup.timessquare;
import android.app.Activity;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import org.intellij.lang.annotations.MagicConstant;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import static com.squareup.timessquare.CalendarPickerView.SelectionMode.MULTIPLE;
import static com.squareup.timessquare.CalendarPickerView.SelectionMode.RANGE;
import static com.squareup.timessquare.CalendarPickerView.SelectionMode.SINGLE;
import static com.squareup.timessquare.MonthCellDescriptor.RangeState.FIRST;
import static com.squareup.timessquare.MonthCellDescriptor.RangeState.LAST;
import static com.squareup.timessquare.MonthCellDescriptor.RangeState.MIDDLE;
import static com.squareup.timessquare.MonthCellDescriptor.RangeState.NONE;
import static java.util.Calendar.APRIL;
import static java.util.Calendar.AUGUST;
import static java.util.Calendar.DATE;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.DAY_OF_WEEK;
import static java.util.Calendar.DECEMBER;
import static java.util.Calendar.FEBRUARY;
import static java.util.Calendar.JANUARY;
import static java.util.Calendar.JULY;
import static java.util.Calendar.JUNE;
import static java.util.Calendar.MARCH;
import static java.util.Calendar.MAY;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.NOVEMBER;
import static java.util.Calendar.OCTOBER;
import static java.util.Calendar.SEPTEMBER;
import static java.util.Calendar.YEAR;
import static org.fest.assertions.api.ANDROID.assertThat;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
@RunWith(RobolectricTestRunner.class)
public class CalendarPickerViewTest {
static {
// Set the default locale to a different one than the locale used for the tests to ensure that
// the CalendarPickerView does not rely on any other locale than the configured one --
// especially not the default locale.
Locale.setDefault(Locale.GERMANY);
}
private Activity activity;
private Locale locale;
private CalendarPickerView view;
private Calendar today;
private Date maxDate;
private Date minDate;
@Before
public void setUp() throws Exception {
activity = Robolectric.buildActivity(Activity.class).create().start().resume().get();
locale = Locale.US;
view = new CalendarPickerView(activity, null);
today = Calendar.getInstance(locale);
today.set(2012, NOVEMBER, 16, 0, 0);
minDate = today.getTime();
today.set(2013, NOVEMBER, 16, 0, 0);
maxDate = today.getTime();
today.set(2012, NOVEMBER, 16, 0, 0);
Date startDate = today.getTime();
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(startDate);
// Do not change the internal state of the CalendarPickerView until init() has run.
view.today.setTime(startDate);
}
@Test
public void testInitDecember() throws Exception {
Calendar dec2012 = buildCal(2012, DECEMBER, 1);
Calendar dec2013 = buildCal(2013, DECEMBER, 1);
view.init(dec2012.getTime(), dec2013.getTime(), locale) //
.inMode(SINGLE) //
.withSelectedDate(dec2012.getTime());
assertThat(view.months).hasSize(12);
}
@Test
public void testInitJanuary() throws Exception {
Calendar jan2012 = buildCal(2012, JANUARY, 1);
Calendar jan2013 = buildCal(2013, JANUARY, 1);
view.init(jan2012.getTime(), jan2013.getTime(), locale) //
.inMode(SINGLE) //
.withSelectedDate(jan2012.getTime());
assertThat(view.months).hasSize(12);
}
@Test
public void testInitMidyear() throws Exception {
Calendar may2012 = buildCal(2012, MAY, 1);
Calendar may2013 = buildCal(2013, MAY, 1);
view.init(may2012.getTime(), may2013.getTime(), locale) //
.inMode(SINGLE) //
.withSelectedDate(may2012.getTime());
assertThat(view.months).hasSize(12);
}
@Test
public void testOnlyShowingFourWeeks() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(FEBRUARY, 2015, today);
assertThat(cells).hasSize(4);
// Last cell should be 1.
assertCell(cells, 0, 0, 1, true, false, false, false, NONE);
// Last cell should be 28.
assertCell(cells, 3, 6, 28, true, false, false, false, NONE);
}
@Test
public void testOnlyShowingFiveWeeks() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(FEBRUARY, 2013, today);
assertThat(cells).hasSize(5);
// First cell is the 27th of January.
assertCell(cells, 0, 0, 27, false, false, false, false, NONE);
// First day of Feb falls on the 5th cell.
assertCell(cells, 0, 5, 1, true, false, false, true, NONE);
// Last day of Feb falls on the 5th row, 5th column.
assertCell(cells, 4, 4, 28, true, false, false, true, NONE);
// Last cell should be March 2nd.
assertCell(cells, 4, 6, 2, false, false, false, false, NONE);
}
@Test
public void testWeirdOverlappingYear() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(JANUARY, 2013, today);
assertThat(cells).hasSize(5);
}
@Test
public void testShowingSixWeeks() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(DECEMBER, 2012, today);
assertThat(cells).hasSize(6);
// First cell is the 27th of November.
assertCell(cells, 0, 0, 25, false, false, false, false, NONE);
// First day of December falls on the 6th cell.
assertCell(cells, 0, 6, 1, true, false, false, true, NONE);
// Last day of December falls on the 6th row, 2nd column.
assertCell(cells, 5, 1, 31, true, false, false, true, NONE);
// Last cell should be January 5th.
assertCell(cells, 5, 6, 5, false, false, false, false, NONE);
}
@Test
public void testIsSelected() throws Exception {
Calendar nov29 = buildCal(2012, NOVEMBER, 29);
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(NOVEMBER, 2012, nov29);
assertThat(cells).hasSize(5);
// Make sure the cell is selected when it's in November.
assertCell(cells, 4, 4, 29, true, true, false, true, NONE);
cells = selectDateAndGetCells(DECEMBER, 2012, nov29);
assertThat(cells).hasSize(6);
// Make sure the cell is not selected when it's in December.
assertCell(cells, 0, 4, 29, false, false, false, false, NONE);
}
@Test
public void testTodayIsToday() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(NOVEMBER, 2012, today);
assertCell(cells, 2, 5, 16, true, true, true, true, NONE);
}
@Test
public void testSelectabilityInFirstMonth() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(NOVEMBER, 2012, today);
// 10/29 is not selectable because it's in the previous month.
assertCell(cells, 0, 0, 28, false, false, false, false, NONE);
// 11/1 is not selectable because it's < minDate (11/16/12).
assertCell(cells, 0, 4, 1, true, false, false, false, NONE);
// 11/16 is selectable because it's == minDate (11/16/12).
assertCell(cells, 2, 5, 16, true, true, true, true, NONE);
// 11/20 is selectable because it's > minDate (11/16/12).
assertCell(cells, 3, 2, 20, true, false, false, true, NONE);
// 12/1 is not selectable because it's in the next month.
assertCell(cells, 4, 6, 1, false, false, false, false, NONE);
}
@Test
public void testSelectabilityInLastMonth() throws Exception {
List<List<MonthCellDescriptor>> cells = selectDateAndGetCells(NOVEMBER, 2013, today);
// 10/29 is not selectable because it's in the previous month.
assertCell(cells, 0, 0, 27, false, false, false, false, NONE);
// 11/1 is selectable because it's < maxDate (11/16/13).
assertCell(cells, 0, 5, 1, true, false, false, true, NONE);
// 11/15 is selectable because it's one less than maxDate (11/16/13).
assertCell(cells, 2, 5, 15, true, false, false, true, NONE);
// 11/16 is not selectable because it's > maxDate (11/16/13).
assertCell(cells, 2, 6, 16, true, false, false, false, NONE);
}
@Test
public void testInitSingleWithMultipleSelections() throws Exception {
List<Date> selectedDates = new ArrayList<Date>();
selectedDates.add(minDate);
// This one should work.
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDates(selectedDates);
// Now add another date and try init'ing again in SINGLE mode.
Calendar secondSelection = buildCal(2012, NOVEMBER, 17);
selectedDates.add(secondSelection.getTime());
try {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDates(selectedDates);
fail("Should not have been able to init() with SINGLE mode && multiple selected dates");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testNullInitArguments() throws Exception {
final Date validDate = today.getTime();
try {
view.init(validDate, validDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(null);
fail("Should not have been able to pass in a null startDate");
} catch (IllegalArgumentException expected) {
}
try {
view.init(null, validDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(validDate);
fail("Should not have been able to pass in a null minDate");
} catch (IllegalArgumentException expected) {
}
try {
view.init(validDate, null, locale) //
.inMode(SINGLE) //
.withSelectedDate(validDate);
fail("Should not have been able to pass in a null maxDate");
} catch (IllegalArgumentException expected) {
}
try {
view.init(validDate, validDate, null) //
.inMode(SINGLE) //
.withSelectedDate(validDate);
fail("Should not have been able to pass in a null locale");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testZeroDates() throws Exception {
final Date validDate = today.getTime();
final Date zeroDate = new Date(0L);
try {
view.init(validDate, validDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(zeroDate);
fail("Should not have been able to pass in a zero startDate");
} catch (IllegalArgumentException expected) {
}
try {
view.init(zeroDate, validDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(validDate);
fail("Should not have been able to pass in a zero minDate");
} catch (IllegalArgumentException expected) {
}
try {
view.init(validDate, zeroDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(validDate);
fail("Should not have been able to pass in a zero maxDate");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testMinAndMaxMixup() throws Exception {
final Date minDate = today.getTime();
today.add(YEAR, -1);
final Date maxDate = today.getTime();
try {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(minDate);
fail("Should not have been able to pass in a maxDate < minDate");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testSelectedNotInRange() throws Exception {
final Date minDate = today.getTime();
today.add(YEAR, 1);
final Date maxDate = today.getTime();
today.add(YEAR, 1);
Date selectedDate = today.getTime();
try {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(selectedDate);
fail("Should not have been able to pass in a selectedDate > maxDate");
} catch (IllegalArgumentException expected) {
}
today.add(YEAR, -5);
selectedDate = today.getTime();
try {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(selectedDate);
fail("Should not have been able to pass in a selectedDate < minDate");
} catch (IllegalArgumentException expected) {
}
}
/**
* Verify the expectation that the set of dates excludes the max.
* In other words, the date interval is [minDate, maxDate)
*/
@Test(expected=IllegalArgumentException.class)
public void testSelectedNotInRange_maxDateExcluded() throws Exception {
view.init(minDate, maxDate, locale)
.inMode(SINGLE)
.withSelectedDate(maxDate);
}
@Test
public void testNotCallingInit() throws Exception {
view = new CalendarPickerView(activity, null);
try {
view.onMeasure(0, 0);
fail("Should have thrown an IllegalStateException!");
} catch (IllegalStateException expected) {
}
}
@Test
public void testShowingOnlyOneMonth() throws Exception {
Calendar feb1 = buildCal(2013, FEBRUARY, 1);
Calendar mar1 = buildCal(2013, MARCH, 1);
view.init(feb1.getTime(), mar1.getTime(), locale) //
.inMode(SINGLE) //
.withSelectedDate(feb1.getTime());
assertThat(view.months).hasSize(1);
}
@Test
public void selectDateThrowsExceptionForDatesOutOfRange() {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(today.getTime());
Calendar outOfRange = buildCal(2015, FEBRUARY, 1);
try {
view.selectDate(outOfRange.getTime());
fail("selectDate should've blown up with an out of range date");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void selectDateReturnsTrueForDateInRange() {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(today.getTime());
Calendar inRange = buildCal(2013, FEBRUARY, 1);
boolean wasAbleToSetDate = view.selectDate(inRange.getTime());
assertThat(wasAbleToSetDate).isTrue();
}
@Test
public void selectDateDoesntSelectDisabledCell() {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(today.getTime());
Calendar jumpToCal = buildCal(2013, FEBRUARY, 1);
boolean wasAbleToSetDate = view.selectDate(jumpToCal.getTime());
assertThat(wasAbleToSetDate).isTrue();
assertThat(view.selectedCells.get(0).isSelectable()).isTrue();
}
@Test
public void testMultiselectWithNoInitialSelections() throws Exception {
view.init(minDate, maxDate, locale) //
.inMode(MULTIPLE);
assertThat(view.selectionMode).isEqualTo(MULTIPLE);
assertThat(view.getSelectedDates()).isEmpty();
view.selectDate(minDate);
assertThat(view.getSelectedDates()).hasSize(1);
Calendar secondSelection = buildCal(2012, NOVEMBER, 17);
view.selectDate(secondSelection.getTime());
assertThat(view.getSelectedDates()).hasSize(2);
assertThat(view.getSelectedDates().get(1)).hasTime(secondSelection.getTimeInMillis());
}
@Test
public void testOnDateConfiguredListener() {
final Calendar testCal = Calendar.getInstance(locale);
view.setDateSelectableFilter(new CalendarPickerView.DateSelectableFilter() {
@Override public boolean isDateSelectable(Date date) {
testCal.setTime(date);
int dayOfWeek = testCal.get(DAY_OF_WEEK);
return dayOfWeek > 1 && dayOfWeek < 7;
}
});
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(today.getTime());
Calendar jumpToCal = Calendar.getInstance(locale);
jumpToCal.setTime(today.getTime());
jumpToCal.add(MONTH, 2);
jumpToCal.set(DAY_OF_WEEK, 1);
boolean wasAbleToSetDate = view.selectDate(jumpToCal.getTime());
assertThat(wasAbleToSetDate).isFalse();
jumpToCal.set(DAY_OF_WEEK, 2);
wasAbleToSetDate = view.selectDate(jumpToCal.getTime());
assertThat(wasAbleToSetDate).isTrue();
}
@Test
public void testWithoutDateSelectedListener() throws Exception {
view.init(minDate, maxDate, locale) //
.inMode(SINGLE) //
.withSelectedDate(today.getTime());
Calendar jumpToCal = Calendar.getInstance(locale);
jumpToCal.setTime(today.getTime());
jumpToCal.add(DATE, 1);
MonthCellDescriptor cellToClick =
new MonthCellDescriptor(jumpToCal.getTime(), true, true, true, true, true, 0,
MonthCellDescriptor.RangeState.NONE);
view.listener.handleClick(cellToClick);
assertThat(view.selectedCals.get(0).get(DATE)).isEqualTo(jumpToCal.get(DATE));
}
@Test
public void testRangeSelectionWithNoInitialSelection() throws Exception {
view.init(minDate, maxDate, locale)
.inMode(RANGE);
assertThat(view.selectedCals).hasSize(0);
assertThat(view.selectedCells).hasSize(0);
Calendar nov18 = buildCal(2012, NOVEMBER, 18);
view.selectDate(nov18.getTime());
assertOneDateSelected();
Calendar nov24 = buildCal(2012, NOVEMBER, 24);
view.selectDate(nov24.getTime());
assertRangeSelected();
assertRangeSelectionBehavior();
}
@Test
public void testInitWithoutHighlightingCells() {
view.init(minDate, maxDate, locale)
.inMode(SINGLE);
assertThat(view.highlightedCals).hasSize(0);
assertThat(view.highlightedCells).hasSize(0);
}
@Test
public void testHighlightingCells() {
final Calendar highlightedCal = buildCal(2012, NOVEMBER, 20);
view.init(minDate, maxDate, locale)
.inMode(SINGLE)
.withHighlightedDate(highlightedCal.getTime());
assertThat(view.highlightedCals).hasSize(1);
assertThat(view.highlightedCells).hasSize(1);
List<List<MonthCellDescriptor>> cells = getCells(NOVEMBER, 2012);
assertThat(cells.get(3).get(2).isHighlighted()).isTrue();
}
private void assertRangeSelectionBehavior() {
// Start a new range in the middle of the current (Nov 18 - Nov 24) one.
Calendar nov20 = buildCal(2012, NOVEMBER, 20);
view.selectDate(nov20.getTime());
assertOneDateSelected();
// Finish that range.
Calendar nov26 = buildCal(2012, NOVEMBER, 26);
view.selectDate(nov26.getTime());
assertRangeSelected();
// Start a new range in the middle of the current (Nov 20 - Nov 26) one.
Calendar nov24 = buildCal(2012, NOVEMBER, 24);
view.selectDate(nov24.getTime());
assertOneDateSelected();
// Only Nov 24 is selected: going backward should start a new range.
view.selectDate(nov20.getTime());
assertOneDateSelected();
}
@Test
public void testRangeWithTwoInitialSelections() throws Exception {
Calendar nov18 = buildCal(2012, NOVEMBER, 18);
Calendar nov24 = buildCal(2012, NOVEMBER, 24);
List<Date> selectedDates = Arrays.asList(nov18.getTime(), nov24.getTime());
view.init(minDate, maxDate, locale)
.inMode(RANGE)
.withSelectedDates(selectedDates);
assertRangeSelected();
assertRangeSelectionBehavior();
}
@Test
public void testRangeWithOneInitialSelection() throws Exception {
Calendar nov18 = buildCal(2012, NOVEMBER, 18);
Calendar nov24 = buildCal(2012, NOVEMBER, 24);
List<Date> selectedDates = Arrays.asList(nov18.getTime());
view.init(minDate, maxDate, locale)
.inMode(RANGE)
.withSelectedDates(selectedDates);
assertOneDateSelected();
view.selectDate(nov24.getTime());
assertRangeSelected();
assertRangeSelectionBehavior();
}
private void assertRangeSelected() {
assertThat(view.selectedCals).hasSize(2);
assertThat(view.selectedCells).hasSize(7);
assertThat(view.getSelectedDates()).hasSize(7);
}
private void assertOneDateSelected() {
assertThat(view.selectedCals).hasSize(1);
assertThat(view.selectedCells).hasSize(1);
assertThat(view.getSelectedDates()).hasSize(1);
}
@Test
public void testRangeStateOnDateSelections() {
Calendar startCal = buildCal(2012, NOVEMBER, 17);
Calendar endCal = buildCal(2012, NOVEMBER, 24);
view.init(minDate, maxDate, locale) //
.inMode(RANGE);
boolean wasAbleToSetDate = view.selectDate(startCal.getTime());
assertThat(wasAbleToSetDate).isTrue();
wasAbleToSetDate = view.selectDate(endCal.getTime());
assertThat(wasAbleToSetDate).isTrue();
List<List<MonthCellDescriptor>> cells = getCells(NOVEMBER, 2012);
assertCell(cells, 2, 6, 17, true, true, false, true, FIRST);
assertCell(cells, 3, 0, 18, true, false, false, true, MIDDLE);
assertCell(cells, 3, 1, 19, true, false, false, true, MIDDLE);
assertCell(cells, 3, 2, 20, true, false, false, true, MIDDLE);
assertCell(cells, 3, 3, 21, true, false, false, true, MIDDLE);
assertCell(cells, 3, 4, 22, true, false, false, true, MIDDLE);
assertCell(cells, 3, 5, 23, true, false, false, true, MIDDLE);
assertCell(cells, 3, 6, 24, true, true, false, true, LAST);
}
@Test
public void testLocaleSetting() throws Exception {
view.init(minDate, maxDate, Locale.GERMAN);
MonthView monthView = (MonthView) view.getAdapter().getView(1, null, null);
CalendarRowView header = (CalendarRowView) monthView.grid.getChildAt(0);
TextView firstDay = (TextView) header.getChildAt(0);
assertThat(firstDay).hasTextString("Mo"); // Montag = Monday
assertThat(monthView.title).hasTextString("Dezember 2012");
}
@Test
public void testFirstDayOfWeekIsMonday() throws Exception {
Locale greatBritain = new Locale("en", "GB");
// Verify that firstDayOfWeek is actually Monday.
Calendar cal = Calendar.getInstance(greatBritain);
assertThat(cal.getFirstDayOfWeek()).isEqualTo(Calendar.MONDAY);
view.init(minDate, maxDate, greatBritain);
MonthView monthView = (MonthView) view.getAdapter().getView(1, null, null);
CalendarRowView header = (CalendarRowView) monthView.grid.getChildAt(0);
TextView firstDay = (TextView) header.getChildAt(0);
assertThat(firstDay).hasTextString("Mon"); // Monday!
List<List<MonthCellDescriptor>> cells = getCells(SEPTEMBER, 2013);
assertThat(cells).hasSize(6);
assertCell(cells, 0, 0, 26, false, false, false, false, NONE);
assertCell(cells, 1, 0, 2, true, false, false, true, NONE);
assertCell(cells, 5, 0, 30, true, false, false, true, NONE);
}
private static void assertCell(List<List<MonthCellDescriptor>> cells, int row, int col,
int expectedVal, boolean expectedCurrentMonth, boolean expectedSelected,
boolean expectedToday, boolean expectedSelectable,
MonthCellDescriptor.RangeState expectedRangeState) {
final MonthCellDescriptor cell = cells.get(row).get(col);
assertThat(cell.getValue()).isEqualTo(expectedVal);
assertThat(cell.isCurrentMonth()).isEqualTo(expectedCurrentMonth);
assertThat(cell.isSelected()).isEqualTo(expectedSelected);
assertThat(cell.isToday()).isEqualTo(expectedToday);
assertThat(cell.isSelectable()).isEqualTo(expectedSelectable);
assertThat(cell.getRangeState()).isEqualTo(expectedRangeState);
}
private List<List<MonthCellDescriptor>> selectDateAndGetCells(int month, int year,
Calendar selectedDate) {
view.selectDate(selectedDate.getTime());
return getCells(month, year);
}
private List<List<MonthCellDescriptor>> getCells(int month, int year) {
Calendar cal = Calendar.getInstance(locale);
cal.set(DAY_OF_MONTH, 1);
cal.set(YEAR, year);
cal.set(MONTH, month);
return view.getMonthCells(new MonthDescriptor(month, year, cal.getTime(), "January 2012"), cal);
}
private Calendar buildCal(int year, @MagicConstant(intValues = {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER,
DECEMBER
}) int month, int day) {
Calendar jumpToCal = Calendar.getInstance(locale);
jumpToCal.set(year, month, day);
CalendarPickerView.setMidnight(jumpToCal);
return jumpToCal;
}
}