Saturday, October 13, 2012

design of datepicker (1000 mcg)

When testing web application, sometime it is necessary to simulate the clicking of a calendar icon and pick a date using JavaScript DatePicker.

DatePicker is no difference from the other element on web page, usually after clicking the calendar icon, a calendar will be popup and most likely it is a division object on the same page, then user looks at the title of the Calendar and decides whether to click previous year or next year, previous month or next month and then click the day.

They are many styles of the calendar right now, most popular JavaScript frameworks such as jQuery, dojo and ext-js provide their own implementations. However, even they have different look and feel, they all have these common properties and behaviors,

1. an icon used as a trigger;
2. display a calendar when the icon is clicked;
3. Previous Year button(<<), optional;
4. Next Year Button(>>), optional;
5. Previous Month button(<);
6. Next Month button(>);
7. day buttons;
8. Weekday indicators;
9. Calendar title i.e. "September, 2011" which representing current month and current year.

Based on these properties and associated behaviors, we can design a generic DatePicker class to be used by all selenium based tests.

/**
 *
 * Copyright (c) 2012, Algocrafts, Inc. All rights reserved.
 * Apache License version 2.
 */
package com.algocrafts.calendar;

/**
 * A general purpose DatePicker can be used to pick a given date from
 * the calendar flyout provided by JavaScript framework.
 *
 * @author Yujun Liang
 * @since 0.1
 */
public class DatePicker {

    private final Calendar calendar;

    /**
     * Constuctor of the DatePicker which taking a Calendar interface.
     *
     * @param calendar
     */
    public DatePicker(Calendar calendar) {
        this.calendar = calendar;
    }

    /**
     * Pick a date by the given parameter.
     * for example,
     * datePicker.pick(AMonthEnum.July, 31, 1999) or for another style,
     * datePicker.pick(AnotherMonthEnum.JULY, 31, 1999), or
     * datePicker.pick(AbbreviatedMonthEnum.Sep, 31, 1999)
     * datePicker.pick(AnotherAbbreviatedMonthEnum.SEPT, 31, 1999)
     * the month just need to be same as the text on the calendar, if it looks like
     * Sep. on the calendar, you can either strip out the dot before the enum valueOf
     * call, or define an enum like this Sep("Sep.") and then provide a lookup map to
     * resolve the enum Sep from "Sep.".
     *
     * @param month, it need to be defined as an enum to make the code cleaner.
     * @param day,   an integer representing the day appearing on the calendar
     * @param year,  an ineger representing the year appearing on the calendar
     */
    public void pick(Enum month, int day, int year) {
        calendar.show();
        calendar.enterYear(year);
        calendar.enterMonth(month);
        calendar.pickDay(day);
    }

The calendar is an interface, it can be implemented based on the DatePicker used by each project.

/**
 *
 * Copyright (c) 2012, Algocrafts, Inc. All rights reserved.
 * Apache License version 2.
 */
package com.algocrafts.calendar;

/**
 * This is the interface describing the behaviour of a calendar flyout
 * which can be operated by a DatePicker.
 *
 * @author Yujun Liang
 * @since 0.1
 */
public interface Calendar {

    /**
     * The action making the calendar visible.
     */
    void show();

    /**
     * 
     * @return current year displayed on Calendar.
     */
    int currentYear();

    /**
     * @return the ordinal of the enum instance for current month on Calendar.
     */
    int currentMonth();

    /**
     * Clicking next year button once, or clicking the next month button
     * 12 times if the next year button is not present on the calendar.
     */
    void nextYear();

    /**
     * Clicking next month button once.
     */
    void nextMonth();

    /**
     * Clicking previous month button once.
     */
    void previousMonth();

    /**
     * Clicking previous year button once, or clicking the previous month
     * button 12 times if the next year button is not present on the calendar.
     */
    void previousYear();

    /**
     * Some calendar allows user to select a year from a dropdown(select) or
     * enter a value from an input field. This method is to cater that function.
     * @param year
     */
    void enterYear(int year);

    /**
     * Some calendar allows user to select a month from a dropdown(select) or
     * enter a value from an input field. This method is to cater that function.
     * @param month
     */
    void enterMonth(Enum month);

    /**
     * After flipping the calendar to the month user is picking, clicking
     * the day button.
     * @param day
     */
    void pickDay(int day);
}

Some calendar does provide direct method to enter year and month, so we need a general purpose Flipping Method to click the buttons to select year and month.
/**
 *
 * Copyright (c) 2012, Algocrafts, Inc. All rights reserved.
 * Apache License version 2.
 */
package com.algocrafts.calendar;

/**
 * For some calender, if no direct handle allow you to change year and month,
 * user need to click the year and month button to flip the calendar to the
 * desired year and month. This class provide such functionality.
 */
public class FlippingMethod {

    private final Calendar calendar;

    /**
     * Associate this with the calendar.
     *
     * @param calendar
     */
    public FlippingMethod(Calendar calendar) {
        this.calendar = calendar;
    }

    /**
     * flip to the year like counting the click by heart with eye closed to save the time
     * of reading the calender again since reading the calendar is more expensive then
     * maitaining a counter.
     *
     * @param year
     */
    public void flipToYear(int year) {
        int yearDiffrence = calendar.currentYear() - year;
        if (yearDiffrence < 0) {
            for (int i = yearDiffrence; i < 0; i++) {
                calendar.nextYear();
            }
        } else if (yearDiffrence > 0) {
            for (int i = 0; i < yearDiffrence; i++) {
                calendar.previousYear();
            }
        }
    }

    /**
     * flip to the month like counting the click by heart with eye closed to save the time
     * of reading the calender again since reading the calendar is more expensive then
     * maitaining a counter.
     *
     * @param month
     */
    public void flipToMonth(Enum month) {
        int monthDifference = calendar.currentMonth() - month.ordinal();
        if (monthDifference < 0) {
            for (int i = monthDifference; i < 0; i++) {
                calendar.nextMonth();
            }
        } else if (monthDifference > 0) {
            for (int i = 0; i < monthDifference; i++) {
                calendar.previousMonth();
            }
        }
    }
}

There is no Selenium used in DatePicker, Calendar and FlippingMethod at all. That's right, this is called Dependency Injection. You may have already heard of this pattern if you have worked on any project using Spring framework or read the blog from Martim Fowler. The following implementation can be injected into DatePicker during runtime and you can inject different Calendar depending on what JavaScript library you are using. You don't even need to use WebDriver, you can still implement your calendar using Selenium-RC which has been deprecated in favor of new WebDriver API in Selenium 2, which is another example of the same principle.

Here is the actual implementation using Selenium WebDriver api against this Datepicker on jQuery's website,  http://jqueryui.com/datepicker/

/**
 *
 * Copyright (c) 2012, Algocrafts, Inc. All rights reserved.
 * Apache License version 2.
 */
package com.algocrafts.calendar;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.List;

import static com.algocrafts.calendar.JQueryCalendar.Months.valueOf;
import static com.google.common.base.Predicates.not;
import static java.lang.Integer.parseInt;
import static org.openqa.selenium.By.className;
import static org.openqa.selenium.By.tagName;

/**
 * This is the reference implmetnation of the Calendar interface which can be
 * operated by a DatePicker.
 * The location of the date picker is here,
 * http://jqueryui.com/datepicker/
 *
 * @author Yujun Liang
 * @since 0.1
 */
public class JQueryCalendar implements Calendar {

    /**
     * An enum of all months used by that calendar
     */
    public enum Months {
        January,
        February,
        March,
        April,
        May,
        June,
        July,
        August,
        September,
        October,
        November,
        December;
    }

    /**
     * Constructor of the JQueryCalendar, an active WebDriver and a search
     * criteria of the trigger element.
     *
     * @param webDriver
     * @param trigger
     */
    public JQueryCalendar(WebDriver webDriver, Function<WebDriver, WebElement> trigger) {
        this.webDriver = webDriver;
        this.trigger = trigger;
    }

    private final WebDriver webDriver;
    private final Function<WebDriver, WebElement> trigger;

        @Override
    public void show() {
        trigger.apply(webDriver).click();
        new WebDriverWait(webDriver, 60).until(new CalendarIsDisplayed());
    }

    @Override
    public int currentYear() {
        return parseInt(calendar().findElement(className("ui-datepicker-year")).getText());
    }

    @Override
    public int currentMonth() {
        return valueOf(
                   calendar().findElement(className("ui-datepicker-month")).getText()
               ).ordinal();
    }

    @Override
    public void previousYear() {
        for (int i = 0; i < 12; i++) {
            previousMonth();
        }
    }

    @Override
    public void previousMonth() {
        calendar().findElement(className("ui-datepicker-prev")).click();
    }

    @Override
    public void nextYear() {
        for (int i = 0; i < 12; i++) {
            nextMonth();
        }
    }

    @Override
    public void nextMonth() {
        calendar().findElement(className("ui-datepicker-next")).click();
    }

    @Override
    public void enterYear(int year) {
        flippingMethod.flipToYear(year);
    }

    @Override
    public void enterMonth(Enum month) {
        flippingMethod.flipToMonth(month);
    }

    @Override
    public void pickDay(int day) {
        List<WebElement> dayButtons =
                calendar().findElement(
                        className("ui-datepicker-calendar")
                ).findElements(tagName("td"));
        for (WebElement dayButton : dayButtons) {
            if (dayButton.getText().equals(String.valueOf(day))) {
                dayButton.click();
                new WebDriverWait(webDriver, 60).until(not(new CalendarIsDisplayed()));
            }
        }
    }

    private WebElement calendar() {
        return webDriver.findElement(By.id("ui-datepicker-div"));
    }

    /**
     * Predicate needed by WebDriverWait for Calendar to become visible
     * it can be used for Calendar to become invisible as well, simply
     *   Predicates.not(new CalendarIsDisplayed()) as used in method 
     *   pickDay.
     */
    private class CalendarIsDisplayed implements Predicate<WebDriver> {
        @Override
        public boolean apply(WebDriver webDriver) {
            return calendar() != null && calendar().isDisplayed();
        }
    }    
}
In your test, where you have access to Selenium WebDriver webDriver, you can simply instantiate the DatePicker,
jQueryCalendarDatePicker = new DatePicker(
                               new JQueryCalendar(
                                   webDriver, 
                                   new JQueryCalendarTriggerFinder()
                               )
                           );
    
jQueryDatePicker.pick(July, 23, 1999);
jQueryDatePicker.pick(September, 2, 2018);
These classes have this relationship as illustrated by the following class diagram,
The test ran and passed, it took a while to go back to 1999 since there is no previous year button available so the behavior of the previous year is actually implemented by clicking previous month 12 times.
If you noticed, the parameter for a date is in 3 parts, an enum of the month, an integer day and integer year, the reason behind it is to make the code more readable, it is the application of Domain Driven Design principle, Ubiquitous Language. I prefer it over a java.util.Date as the parameter. Also, if I had chosen to use java.util.Date as the parameter, the implementation of the DatePicker would have been more complex. As an exercise, you can try that at home.

If you use another JavaScript framework, the calendar may have different elements, you can implement another Calendar and use the same DatePicker to pick any Date in the range provided by the calendar.

Dojo,
DatePicker dojoDatePicker = 
   new DatePicker(new DojoCalendar(webDriver, new DojoCalendarTriggerFinder()));
dojoDatePicker.pick(July, 23, 1999);
dojoDatePicker.pick(September, 2, 2018);

public class DojoCalendar implements Calendar {
   ... //Jonathan, sorry for using ...   
}
YUI,
DatePicker yuiDatePicker = 
   new DatePicker(new YuiCalendar(webDriver,new YuiCalendarTriggerFinder()));

yuiDatePicker.pick(July, 23, 1999);
yuiDatePicker.pick(September, 2, 2018);

public class YuiCalendar implements Calendar {
   ... //sorry again.   
}
Here is another style of the calendar,
since it provides the direct method to pick year and month, the implementation is slightly different from JQueryCalendar, it uses a Select to change the year. So in this class, it doesn't need to instantiate an instance of FlippingMethod.
    @Override
    public void enterYear(int year) {
        yearSelect().selectByVisibleText(String.valueOf(year));
    }

    /**
     * //TODO: use firebug to find out THE NAME OF THE SELECT OF YEAR  
     * @return
     */
    private Select yearSelect() {
        return new Select(webDriver.findElement(By.name("THE NAME OF THE SELECT OF YEAR")));
    }

    @Override
    public void enterMonth(Enum month) {
        monthSelect().selectByVisibleText(month.name());
    }

    /**
     * //TODO: use firebug to find out THE NAME OF THE SELECT OF MONTH  
     * @return
     */
    private Select monthSelect() {
        return new Select(webDriver.findElement(By.name("THE NAME OF THE SELECT OF MONTH")));
    }
Why bother to design this DatePicker class with an interface, not just use inheritance with an abstract PickPicker for shared logic such as flipping calendar to desired year and month?

References:
1. jQuery : http://jquery.com/
2. Selenium : http://seleniumhq.org/
3. Martin Fowler : http://martinfowler.com/
4. Domain Driven Design : http://www.domaindrivendesign.org/

4 comments: