Automated QA: Save time, use a web calendar handler!

Derick Arzu
Nov 12, 2019 · 8 min read

Text boxes, check boxes, radio buttons, and other elements of forms are fairly simple to deal with when it comes to developing UI functional tests for a web application. But what happens when you want to write a test that verifies that the UI for a web calendar is working?

You are probably thinking that it can be easily achieved with a couple of clicks and validations, which is not only true but also the approach that led to the idea of a handler.

Before you discover how to make your automated QA team very happy, here are some reasons why writing a simple function or just a segment of code that deals with ONE specific calendar is not as scalable. Imagine you are a QA Developer at a company who is developing the websites for airlines A and B and your team is requested to create the automated test suites. Sounds quite easy, you will use the same code in both projects, nothing will need to be changed; until someone shows you the designs of the two web calendars each airline uses in their website.

airline A uses this calendar in their website
airline B uses this calendar in their website

Beginning with the obvious differences, airline A uses two windows while airline B uses only one; that surely represents a significant change in the code of that first approach. Another difference you might not have noticed is that airline B has a dropdown to change the year of the calendar, so that would mean a slight change in the method used to get the text of the displayed year. Those are two visual differences that will affect the way your bot interacts with the calendar and the DOM will surely surprise you with more.


Now that you are interested, the coding begins!

This handler was implemented in Node.js and uses WebdriverIO as the test framework that interacts with the browser.

You will find out that the framework has two functions ($ and $$) to fetch web elements. However, a hierarchy of classes will be created to manipulate elements, later on you will learn this is so that the handler can easily be able to cover many web calendar designs.

The main class is Element. Here is where, the method to obtain the fetch function is implemented, it has two parameters:

  1. selectorObject (required) which refers to an object with two properties; the first named selector , is a string that specifies the selector that will be used to fetch the element. The second is index, which is an integer that must be assigned to the object if the fetch result wants to be treated as a single element and not as an array of elements.
  2. additionalProperties (optional) is an object with any property that wants to be added to the fetch result. The subclasses of the hierarchy use this to manage how some data is obtained from the web elements, you will learn this later on.
class Element {
constructor(){
if (this.toString === undefined) {
throw new TypeError('Must override method');
}
}
getFetchFunction(selectorObject, additionalProperties){
return () => {
let result = browser.$$(selectorObject.selector);
if(selectorObject.hasOwnProperty('index')){
result = result[selectorObject.index];
}
Object.assign(result, additionalProperties);
return result;
}
}
}

Two subclasses were implemented after the conclusion that it can be necessary to either view an element as one with a single part (the way it is naturally viewed by the test framework) or one with multiple parts (the header of airline B for example).

Both subclasses have a getter named domRepresentation which returns all the web elements that make up the Element object. Additionally, they have two methods:

  1. toString returns the visible text as a string or an array of strings if this applies, otherwise it throws an error. It has the parameter index which is used in case the fetched result is being treated as an array of elements.
  2. apply is a method that takes an action as parameter, which is a function that executes on the domRepresentation of the Element and may return a condition.

The SinglePartElement class uses the getField additional property so that the user of the handler can decide which function the test framework provides best suites the method to extract the visible text from the element (e.g. getText, getValue, execute from WebdriverIO)

class SinglePartElement extends Element {
constructor(partAttributes){
super();
this.partAttributes = partAttributes;
if(partAttributes.hasOwnProperty('getField')){
this.part = this.getElementFunction(partAttributes, {
getField: partAttributes.getField
});
}
else{
this.part = this.getElementFunction(partAttributes);
}
}
get domRepresentation() { return this.part(); } toString(index){
try{
return this.domRepresentation.getField(index);
}
catch(error){
throw new Error (`Error: getField needed to get text`);
}
}
apply(action){
let condition = action(this.domRepresentation);
if(Array.isArray(condition)){
condition = condition.every(
conditionElement => conditionElement
);
}
return condition;
}
}

The MultiplePartsElement class also uses the getField additional property on every part, additionally it uses the position property; this is to make the parts ordinal. It is very useful when obtaining the visible text of an instance of this class since it is not the same to say 2019 October as October 2019.

class MultiplePartsElement extends Element {
constructor(partsAttributes){
super();
this.parts = [];
for(let partAttributes of partsAttributes){
let partElement = this.getElementFunction(partAttributes, {
getField: partAttributes.getField,
position: partAttributes.position
});
this.parts[partAttributes.position] = partElement;
}
}
get domRepresentation() { return this.parts.map(part => part()); } toString(index){
return this.domRepresentation.map(
partDomRepresentation => partDomRepresentation.getField(index)
);
}
apply(action){
let condition = true;
this.domRepresentation.forEach( partDomRepresentation => {
let partCondition = action(partDomRepresentation);
if(Array.isArray(partCondition)){
partCondition = partCondition.every(
conditionElement => conditionElement
);
}
condition = condition && partCondition;
})
return condition;
}
}

The above hierarchy will help simplify the implementation of the web calendar handler.

The handler is a class with many functions to interact with a visible web calendar and obtain data from it. Each function will be explained for a better understanding.

class WebCalendar {  constructor(elements){
this.elements = elements;
}

The constructor is quite simple, it receives an object with all the Element properties that make up the calendar. These properties may be any of the following:

  • header refers to the part of the calendar that indicates month and year being displayed in a window of the calendar.
  • nextButton refers to the part of the calendar that allows the navigation to a window that contains months ahead of the months displayed in the current window.
  • previousButton refers to the part of the calendar that allows the navigation to a window that contains months behind of the months displayed in the current window.
  • dates refers to the part of the calendar showing the dates for a specific month and year being displayed in a window of the calendar.
  get headersText() {
let requiredElement = 'header';
if(!this.elements.hasOwnProperty(requiredElement)){
throw new Error(`Error: A ${requiredElement} missing`);
}
let header = this.elements.header;
if(Array.isArray(header)){
return header.map(headerElement => {
let headerText = headerElement.toString();
if(Array.isArray(headerText)){
return headerText.join();
}
return headerText;
})
}
let headerText = header.toString();
if(Array.isArray(headerText)){
return headerText.join(' ')
}
return headerText;
}

The class has a getter method named headersText, which returns the text displayed by the Element that represents the header. If the web calendar has more than one header (such is the case of that used by airline A), an array of the displayed texts will be returned instead. The validation for headerText is because a MultiplePartsElement object returns the text of each part in an array.

  headerSectionVisible() {
let requiredElement = 'header';
if(!this.elements.hasOwnProperty(requiredElement)){
throw new Error(`Error: A ${requiredElement} missing`);
}
let header = this.elements.header;
if(Array.isArray(header)){
return header.every(
headerElement => headerElement.apply(
header => header.isDisplayed()
)
);
}
return header.apply(header => header.isDisplayed());
}

headerSectionVisible is a rather simple function that verifies that all headers in the calendar are visible. It uses the function apply which was explained above, to perform an action that calls the isDisplayed method of the API of WebdriverIO. This function is called by other functions of the handler before interacting with any of the headers of the calendar.

  textFoundInHeaderSection(text){
if(Array.isArray(this.headersText)){
return this.headersText.some(
headerText => headerText == text
)
}
return this.headersText == text;
}

The function textFoundInHeaderSection is to verify whether some text is included in any of the headers of the calendar. It calls the headersText getter to obtain that data needed from the web calendar.

  findMonth(date, searchTarget, navIndexes = {prev: 0, next: 0}){
let searchTargetValue = searchTarget(date);
browser.waitUntil(() => this.headerSectionVisible());
let referenceDate;
if(Array.isArray(this.headersText)){
referenceDate = new Date(this.headersText[0]);
}
else{
referenceDate = new Date(this.headersText);
}
while(!this.textFoundInHeaderSection(searchTargetValue)){
if(referenceDate < date){
this.clickNextButton(navIndexes.next)
}
else{
this.clickPreviousButton(navIndexes.prev);
}
browser.waitUntil(() => this.headerSectionVisible());
}
if(Array.isArray(this.headersText)){
return this.headersText.indexOf(searchTargetValue)
}
return 0;
}

findMonth is a method that uses data present in the windows of the calendar, along with input from the user to navigate through the calendar and locate the window where the targeted month is displayed. Also, it returns the index of the calendar window that contains the targeted month. The user must input the following:

  • date refers to a Date object that specifies which is the date that is being searched for. For example new Date(Dec 25, 2019).
  • searchTarget refers to a function that accepts a Date object as a parameter and returns a string that will indicate the handler when the targeted month is already displayed in a window of the calendar. For example date => `${date.getMonth()} ${date.getYear()}`.
  • navIndexes (optional) refers to an object that indicates which navigators of the calendar will be used. The default behavior is to use the first previous and next navigators. An example can be seen in the definition of the optional parameter.
  chooseDate(index, getMonthYear, datesIndex = 0, headerIndex = 0){
let requiredElement = 'dates';
if(!this.elements.hasOwnProperty(requiredElement)){
throw new Error(`Error: A ${requiredElement} missing`);
}
let calendarHeader;
if(Array.isArray(this.headersText)){
calendarHeader = this.headersText[headerIndex];
}
else{
calendarHeader = this.headersText;
}
let {month, year} = getMonthYear(calendarHeader);
let dates = this.elements.dates;
let date;
if(Array.isArray(dates)){
date = dates[datesIndex].toString()[index];
dates[datesIndex].apply(dates => dates[index].click());
}
else{
date = dates.toString(index);
dates.apply(dates => dates[index].click());
}
let chosenDate = new Date(`${month} ${date}, ${year}`);
return chosenDate;
}

The chooseDate function is where the ultimate click is done. It uses input from the user to know which date of the dates array must be clicked. Additionally, data from the calendar is used to return the chosen date. The user must input the following:

  • index refers to the position of the targeted date in the array of dates of one window of the calendar. For example 42
  • getMonthYear refers to a function that accepts an integer as an optional parameter and returns the month and year shown in a window of the calendar. For example header => { month: header.split(‘ ’)[0], year: header.split(‘ ’)[1] }
  • datesIndex (optional) refers to a position in an array of dates arrays in case more than one array of dates is being used and that needs to be specified. It defaults to 0. For example 1.
  • headerIndex (optional) refers to a position in an array of headers in case more than one header make up the web calendar and that needs to be specified. It defaults to 0. For example 2.
  clickPreviousButton(index = 0){
let requiredElement = 'previousButton';
if(!this.elements.hasOwnProperty(requiredElement)){
throw new Error(`Error: A ${requiredElement} missing`);
}
let previousButton = this.elements.previousButton;
if(Array.isArray(previousButton)){
previousButton[index].apply(
previousButton => previousButton.click()
)
}
else{
previousButton.apply(
previousButton => previousButton.click()
);
}
}
clickNextButton(index = 0){
let requiredElement = 'nextButton';
if(!this.elements.hasOwnProperty(requiredElement)){
throw new Error(`Error: A ${requiredElement} missing`);
}
let nextButton = this.elements.nextButton;
if(Array.isArray(nextButton)){
nextButton[index].apply(nextButton => nextButton.click())
}
else{
nextButton.apply(nextButton => nextButton.click());
}
}
}

clickPreviousButton and clickNextButton are two functions that accept an integer as an optional parameter to click a specific navigation button to move to windows that contain months ahead or behind those contained in current windows.

That wraps up the implementation of a versatile web calendar handler. Do not hesitate to try it yourself!

I look forward to hearing comments and answer any inquiry. Also, receiving feedback to make any necessary improvements would be highly appreciated.

I hope you enjoyed it!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade