Writing Selenium Tests for Checking Bookmark Feature and Search functionality in Open Event Webapp

We integrated Selenium Testing in the Open Event Webapp and are in full swing in writing tests to check the major features of the webapp. Tests help us to fix the issues/bugs which have been solved earlier but keep on resurging when some new changes are incorporated in the repo. I describe the major features that we are testing in this.

Bookmark Feature
The first major feature that we want to test is the bookmark feature. It allows the users to mark a session they are interested in and view them all at once with a single click on the starred button. We want to ensure that the feature is working on all the pages.

Let us discuss the design of the test. First, we start with tracks page. We select few sessions (2 here) for test and note down their session_ids. Finding an element by its id is simple in Selenium can be done easily. After we find the session element, we then find the mark button inside it (with the help of its class name) and click on it to mark the session. After that, we click on the starred button to display only the marked sessions and proceed to count the number of visible elements on the page. If the number of visible session elements comes out to be 2 (the ones that we marked), it means that the feature is working. If the number deviates, it indicates that something is wrong and the test fails.

82080522-21e8-403d-9906-3b4f420720b9.png

Here is a part of the code implementing the above logic. The whole code can be seen here

// Returns the number of visible session elements on the tracks page
TrackPage.getNoOfVisibleSessionElems = function() {
 return this.findAll(By.className('room-filter')).then(this.getElemsDisplayStatus).then(function(displayArr) {
   return displayArr.reduce(function(counter, value) { return value == 1 ? counter + 1 : counter; }, 0);
 });
};
// Bookmark the sessions, scrolls down the page and then count the number of visible session elements
TrackPage.checkIsolatedBookmark = function() {
 // Sample sessions having ids of 3014 and 3015 being checked for the bookmark feature
 var sessionIdsArr = ['3014', '3015'];
 var self = this;
 return  self.toggleSessionBookmark(sessionIdsArr).then(self.toggleStarredButton.bind(self)).then(function() {
   return self.driver.executeScript('window.scrollTo(0, 400)').then(self.getNoOfVisibleSessionElems.bind(self));
 });
};

Here is the excerpt of code which matches the actual number of visible session elements to the expected number. You can view the whole test script here

//Test for checking the bookmark feature on the tracks page
it('Checking the bookmark toggle', function(done) {
 trackPage.checkIsolatedBookmark().then(function(num) {
   assert.equal(num, 2);
   done();
 }).catch(function(err) {
   done(err);
 });
});

Now, we want to test this feature on the other pages: schedule and rooms page. We can simply follow the same approach as done on the tracks page but it is time expensive. Checking the visibility of all the sessions elements present on the page takes quite some time due to a large number of sessions. We need to think of a different approach.We had already marked two elements on the tracks page. We then go to the schedule page and click on the starred mode. We calculate the current height of the page. We then unmark a session and then recalculate the height of the page again. If the bookmark feature is working, then the height should decrease. This determines the correctness of the test. We follow the same approach on the rooms pages too. While this is not absolutely correct, it is a good way to check the feature. We have already employed the perfect method on the tracks page so there was no need of applying it on the schedule and the rooms page since it would have increased the time of the testing by a quite large margin.

Here is an excerpt of the code. The whole work can be viewed here

RoomPage.checkIsolatedBookmark = function() {
 // We go into starred mode and unmark sessions having id 3015 which was marked previously on tracks pages. If the bookmark feature works, then length of the web page would decrease. Return true if that happens. False otherwise
 var getPageHeight = 'return document.body.scrollHeight';
 var sessionIdsArr = ['3015'];
 var self = this;
 var oldHeight, newHeight;
 return self.toggleStarredButton().then(function() {
   return self.driver.executeScript(getPageHeight).then(function(height) {
     oldHeight = height;
     return self.toggleSessionBookmark(sessionIdsArr).then(function() {
       return self.driver.executeScript(getPageHeight).then(function(height) {
         newHeight = height;
         return oldHeight > newHeight;
       });
     });
   });
 });
};

Search Feature
Now, let us go to the testing of the search feature in the webapp. The main object of focus is the Search bar. It is present on all the pages: tracks, rooms, schedule, and speakers page and allows the user to search for a particular session or a speaker and instantly fetches the result as he/she types.

We want to ensure that this feature works across all the pages. Tracks, Rooms and Schedule pages are similar in a way that they display all the session of the event albeit in a different manner. Any query made on any one of these pages should fetch the same number of session elements on the other pages too. The speaker page contains mostly information about the speakers only. So, we make a single common test for the former three pages and a little different test for the latter page.

Designing a test for this feature is interesting. We want it to be fast and accurate. A simple way to approach this is to think of the components involved. One is the query text which would be entered in the search input bar. Other is the list of the sessions which would match the text entered and will be visible on the page after the text has been entered. We decide upon a text string and a list containing session ids. This list contains the id of the sessions should be visible on the above query and also contain few id of the sessions which do not match the text entered. During the actual test, we enter the decided text string and check the visibility of the sessions which are present in the decided list. If the result matches the expected order, then it means that the feature is working well and the test passes. Otherwise, it means that there is some problem with the default implementation and the test fails.

For eg: We decide upon the search text ‘Mario’ and then note the ids of the sessions which should be visible in that search.

c0e4910f-cf69-4b2a-8cc1-233badb35eee.png

Suppose the list of the ids come out to be

['3017''3029''3013''3031']

We then add few more session ids which should not be visible on that search text. Like we add two extra false ids 3014, 3015. Modified list would be something like this

['3017''3029''3013''3031''3014''3015']

Now we run the test and determine the visibility of the sessions present in the above list, compare it to the expected output and accordingly determine the fate of the test.

Expected: [truetruetruetruefalsefalse]
Actual Output: [truetruetruetruetruetrue]

Then the test would fail since the last two sessions were not expected to be visible.

Here is some code related to it. The whole work can be seen here

function commonSearchTest(text, idList) {
 var self = this;
 var searchText = text || 'Mario';
 // First 4 session ids should show up on default search text and the last two not. If no idList provided for testing, use the idList for the default search text
 var arrId = idList || ['3017', '3029', '3013', '3031', '3014', '3015'];
 var promise = new Promise(function(resolve) {
   self.search(searchText).then(function() {
     var promiseArr = arrId.map(function(curElem) {
       return self.find(By.id(curElem)).isDisplayed();
     });

     self.resetSearchBar().then(function() {
       resolve(Promise.all(promiseArr));
     });
   });
 });
 return promise;
}

Here is the code for comparing the expected and the actual output. You can view the whole file here

it('Checking search functionality', function(done) {
 schedulePage.commonSearchTest().then(function(boolArr) {
   assert.deepEqual(boolArr, [true, true, true, true, false, false]);
   done();
 }).catch(function(err) {
   done(err);
 });
});

The search functionality test for the speaker’s page is done in the same style. Just instead of having the session ids, we work with speaker ids there. Rest everything is done in a similar manner.

Resources:

Building a showcase site to display sample events and auto deploying them on each PR merge in Open Event Webapp

Open Event Webapp generates static websites of the event fed to it in the form of JSON data. Earlier, we used to automatically deploy the FOSSASIA Summit event website on the Github pages of the repository on every merge of a pull request.  The presence of it helped in showcasing and testing purposes. But a single event was not enough. Events are diverse, taking place in a large number of rooms, h a variety of sessions and extending over several days. So, having multiple events on the showcase site and continuously showcasing and testing them was a really great idea.

Here is the feature request for it. I will be explaining the major portions of it. The whole code can be found here

First of all, we need to build the main index page which we will showcase our sample events. It will contain the small images of the event and on clicking them, the selected event will be opened. For displaying features and capability of the generator,  we have two separate sections of events: one having the single session page and the other having the expandable sessions page. There will be links to the other components of the Open Event Ecosystem like Android App generator,  Web App generatorOrganizer App, and Eventyay.

The contents of that page are kept in the overviewSite folder. It contains the event background images of the events and the main index file. The basic structure of the file is shown below. The whole file can be viewed here

<div class="container-fluid bg-3 text-center">
 <div class="row margin">
   <div class="col-md-6 col-sm-12 col-xs-12">
     <div class="row">
       <h2 class="margin"> Apps with expandable sessions page </h2>
       <div class="col-md-6 col-sm-6 col-xs-12 margin">
         <p><strong>Open Tech Summit 2017</strong></p>
         <a href='./OpenTechSummit/index.html' target='_blank'>
           <img src="./otssmall.jpg" class=""  alt="Image">
         </a>
       </div>
      </div>
    </div>
   <div class="col-md-6 col-sm-12 col-xs-12">
     <div class="row">
       <h2 class="margin"> Apps with single sessions page </h2>
       <div class="col-md-6 col-sm-6 col-xs-12 margin">
         <p><strong>Mozilla All Hands 2017</strong></p>
         <a href='./MozillaAllHands2017/index.html' target='_blank'>
           <img src="./mozilla_banner.jpg" class=""  alt="Image">
         </a>
       </div>
     </div>
   </div>
 </div>
</div>

But, this is just the front end of the page. We haven’t generated the sample events yet and neither have we made any changes to make them auto deploy on each PR merge. The test script of the app which contains unit, acceptance and selenium tests runs on each PR made against the repo when Travis CI build is triggered. So, it makes sense to write code for generating the event there itself. When the test script has finished executing, all of the events would have been generated and present inside a single folder named [email protected] (We needed something short and easy to remember and the name doesn’t really matter). We then copy the contents of the overviewSite folder into the above folder. It already contains the folder of different sample events.

Here is the related code. The full test script file can be found here

describe('generate', function() {
 describe('.create different event sites and copy assets of overview site', function() {
   // Sample events are generated inside the [email protected] folder
   it('should generate the Mozilla All Hands 2017', function(done) {
     var data = {};
     // API endpoint configuration of the All Hands 2017 event and the session style set to single pages
     data.body = {
       "email": "[email protected]",
       "name": "Open Event",
       "apiendpoint":    "https://raw.githubusercontent.com/fossasia/open-event/master/sample/MozillaAllHands17",
       "sessionMode": "single",
       "datasource": "eventapi",
       "assetmode" : "download"
     };
     generator.createDistDir(data, 'Socket', function(appFolder) {
       assert.equal(appFolder, "[email protected]/MozillaAllHands2017");
       done();
     });
   });
   // For copying the static files required for the showcase site
   it('should copy all the static files', function(done) {
     var staticPath = __dirname + '/../src/backend/overviewSite/';
     function copyStatic(fileName) {
       // Copy the static files in the overviewSite folder to the [email protected] folder
     }
   });
 });
});

Everything is almost done now. We then just make some changes in the deploy script to publish the whole folder containing the different event samples instead of a particular sample and everything works fine.

We navigate to [email protected] inside the dist directory and initialize a git repository there. We then set the username and email and fetch the contents of the gh-pages branch of the official repo (using the Github token. It is defined in the Travis Settings as a private environment variable) and reset it. We then add all the files present in the current directory to the staging area, commit them and push it to the upstream repository. Here is the relevant code. The whole github deploy script file can be viewed here

eval cd [email protected]/
git init
git config --global user.name "Travis CI"
git config --global user.email "[email protected]"
git remote add upstream "https:[email protected]/"${TRAVIS_REPO_SLUG}".git"
git fetch upstream
git reset upstream/gh-pages
touch .
git add -A .
git commit -m "rebuild pages at ${rev}"
git push -q upstream HEAD:gh-pages

Here is the screenshot showing the successful deployment of the sample events. You can view the whole log here. A screenshot of it is below. You can view the higher quality image by clicking on it.

29e53dc0-8841-4329-8a37-8d2e9f08756c.png

After all the work, this is how the showcase page looks like. Samples are re-generated and the site is auto-deployed on every Pull Request merged in the repository.

66804d19-6862-494f-80b6-e1749493e780.png

On clicking on any of the listed events, we jump to the index page of that particular event. Like, If we click on the Facebook Developer Conference, a new tab is opened and we are taken to the front page of that event!

6a47f623-82c6-49e2-af1c-4d152aaaf9b7.png

References

Implementing Single Session Pages in Open Event Webapp

Recently, we implemented a feature request in the Open Event Webapp which allows the organizers of an event to choose the style of the sessions displayed on the event web site. Before this feature of individual session pages was implemented, we had a single default style of displaying collapsible session elements on the page. A small preview of the session containing its title, type, name, position, and picture of the speaker(s) presenting it would be shown on the different pages. When the user will click on this session element, it would collapse showing detailed information about it.

Before Clicking

2c12db41-d999-423a-bdee-6b639cbdbb0b.png

After Clicking on the first session element

fe1a3362-ebad-42a0-ba9d-0909c2de21a9.png

While this default behavior of collapse of session element on click (shown above) works well in most of the cases, it might not be apt in situations where the session contains a large amount of detail and a higher number of speakers presenting it. Single session pages would work better in that case.

So, we provided an option to select it on the generator form itself. We provided an input field asking which session style does the organizer want? Is it the single session style or the expandable session? The organizer can then select one of them and the site will be generated according to that!!

2989ee1f-509c-400d-94cd-023a06a3601d.png

The whole work was huge and you can view all of it here. I will only be describing the major parts of it here.

The first challenge was to make a template (handlebars) file for the individual sessions. This template would take a single session object as an input. The object would contain all the details about the session and after compilation during generation, a unique individual page for that session would be created.

Here is the basic structure of the template. You can view the whole file here

<div class="container session-container">
 <!-- Contains all the information about the session -->
 <div class="row single-session" id="{{session_id}}">
   <!-- Displaying the date, start and end time of the session -->
   <h3> {{startDay}} </h3>
   <div class = "eventtime"><span class="time-track">{{start}} - {{end}}</span></div>
   <div class="session-content">
     <div class="sizeevent event" id="event-title">
       <!-- Display the title, type and the track of the session -->
     </div>
     <div>
       <!-- Short abstract of the session -->
       {{{description}}}
       <div class="session-speakers-list">
         <!-- Contains detailed information about the speakers of the session. Display their picture, show their position, short biography and social links links like Github, Twitter and LinkedIn -->
       </div>
     </div>
   </div>
 </div>
</div>

But the work is not completed yet. We have to check the style of the session selected by the organizer and if he/she has selected the single session option, then we have to pass all the sessions to this template file during the generation process and create the pages. If the mode selected is expandable, then we carry out the normal generation procedure. Else, we extract every session from the JSON data and feed into to the above template. Since the number of sessions can be quite large, we don’t generate them alongside the other pages like tracks, schedule, rooms, speakers and so on. Instead, we create a new folder named sessions and put in all of the new individual pages there in one place. It helps to keep the directory clean and modularized. Also, since we are placing it inside of a session folder, we will have to update the links to the main pages in the navbar section. Like, instead of track.html, it will be ../tracks.html now. The file is given a common format name of session_sessionId where sessionId is the id of that particular session.

Here is the related code. It is taken from generator.js file in the project

function templateGenerate() {
 if(mode == 'single') {
   function checkLinks() {
     // Made necessary modifications in the links to the main pages
   }
   //jsonData contains all the information about the event
   var trackArr = jsonData.tracks;
   for(var i = 0; i < trackArr.length; i++) {
     var sessionArr = trackArr[i].sessions;
     for(var j = 0; j < sessionArr.length; j++) {
       var sessionObj = JSON.parse(JSON.stringify(sessionArr[j]));
       // Do some modifications in the sessionObj to include the track background and font color and additional links
     } 
   }
   var data = {session: sessionObj};
   checkLinks();
   // Pass the session object to the template, compile and minify the HTML file and place into the sessions folder
   fs.writeFileSync(distHelper.distPath + '/' + appFolder + '/sessions/session_' + sessionId + '.html', minifyHtml(sessiontpl(data)));
 }
}

Ok, most of the things are done now. Just one simple step is missing. When the user clicks on the small session element on the tracks, rooms or the schedule page, then we collapse the session element or open up a brand new session page for showing detailed information about the session depending upon what option the organizer has selected. So, we will have to make little appropriate change for handling this as well. Below code is taken from the tracks page template file

<div class = "room-filter">
 {{#if ../../../mode}}
   <div class="sizeevent event">
 {{else}}
   <div class="sizeevent event" data-toggle="collapse" data-target="#desc-{{session_id}}, #desc2-{{session_id}}">
 {{/if}}
</div>

$('.room-filter').click(function () {
 var sessionMode = "{{mode}}";
 var id =  $(this).attr('id');
 if(sessionMode == 'single') {
   var curUrl = window.location.href;
   var newUrl = curUrl.substring(0, curUrl.lastIndexOf('/') + 1) + 'sessions/session_' + id + ".html";
   window.location.href = newUrl;
 }
});

We are simply checking the mode and if it is set, it means that the session style is a single page. So, we don’t include the bootstrap collapse classes in that case. We handle that click event in the javascript part and appropriate redirect the user to the unique page for that session.

So, after all this hard work, this is how it looks like.

Before clicking

0fdb3641-2579-4537-8f36-910c636cf172.png

After clicking on the first session element, a new page is opened

e5d2ed0d-c54a-44fd-b30c-af440936c8ed.png

Resources

Implementing Session Bookmark Feature in Open Event Webapp

The event websites generated by Open Event Webapp can be large in size with a massive number of sessions in it. As such, it becomes a little difficult for the user to keep track of the sessions he is interested in without any bookmark facility.  With the help of it, he/she can easily mark a session as his favorite and then see them all at once at the click of the button. This feature is available and synchronized across all the pages (track, rooms, and schedule) of the event. That is, if you bookmark a session on the tracks page, it would automatically appear on the other pages too. Likewise, if you unmark a session on any of the page, it would be reflected on rest of the pages.

The event websites generated by the webapp are static in nature. It means that there is no backend server and database where the data about the bookmarked sessions and the user can be stored. So how are we storing information about the marked sessions and persisting it across different browser sessions? There must be a place where we are storing the data! Yes. It’s the LocalStorage Object. HTML5 introduced a localStorage object for storing information which persists even when the site or the browser is closed and opened again. Also, the max size of the localStorage object is quite sufficient (around 5 – 10 MB) for storing textual data and should not be a bottleneck in the majority of the cases.

One very important thing to remember about the localStorage object is that it only supports strings in key and value pairs. Hence, we can’t store any other data type like Boolean, Number or Date directly in it. We would have to convert the concerned object into a string using the JSON.stringify() method and then store it.

Okay, let’s see how we implement the bookmark facility of sessions. We initialize the localStorage object. We create a key in it which is the name of the event and corresponding to its value, we store an object which will contain the information about the bookmarked sessions of that particular event. It’s key will be the id of the session and the value (0 or 1) will define whether it is bookmarked or not. Initially, when no sessions have been bookmarked, it will be empty.

function initPage() {
 //Check whether the localStorage object is already instantiated

 if(localStorage.hasOwnProperty(eventName) === false) {
   localStorage[eventName] = '{}'; // This object will store the bookmarked sessions info
 }
}

Now, we need to actually provide a button to the user for marking the sessions and then link it to the localStorage object. This is the session element with the star-shaped bookmark button

b77bf0d9-7303-4c03-ae29-bd8a4725002b.png

Here is the related code

<a class="bookmark" id="b{{session_id}}">
 <i class="fa fa-star" aria-hidden="true">
 </i>
</a>

Then we need to handle the click event which occurs when the user clicks this button. We will extract the id of the session and then store this info in the event object inside the localStorage object.

$('.bookmark').click(function (event) {
   // Get the event object from the localStorage
   var temp = JSON.parse(localStorage[eventName]);

   // Get the current color of the bookmark icon. If it's black, it means that the session is already bookmarked.
   var curColor = $(this).css("color");

   // Extract the id of the session
   var id = $(this).attr('id').substring(1);

   if (curColor == "rgb(0, 0, 0)") {
     // Unmark the session and revert the font-icon color to default
     temp[id] = 0;
     $(this).css("color", "");
   } else {
     // Bookmark the session and color the font icon black
     temp[id] = 1;
     $(this).css("color", "black");
   }

   localStorage[eventName] = JSON.stringify(temp);
   event.stopPropagation();
 });

So suppose, our event name is FOSSASIA Summit and we bookmarked sessions with the id of 2898 and 3100 respectively, then the structure of the localStorage object will be something like this:-

{'FOSSASIA Summit': { '2898': '1', '3100': '1'}};

To finally show all the bookmarked sessions at once, we defined two modes of the page: Starred and All. In the Starred mode, only the bookmarked sessions are shown. We simply select the event object inside the localStorage and iterate over all the session id’s and only show those which have their value set to 1. In the latter mode, all the sessions are displayed.

Starred Mode (Showing only the bookmarked sessions)

ca1f5fa8-29a2-4b96-8eb1-b22d8b07bda8.png

All Mode (Every session is displayed, both marked and unmarked)

cd23f5f5-4bd1-4d1e-aa96-f4aa28cb5e07.png

References:

Implementing PNG Export of Schedule in Open Event Webapp

Fortunately for us, we don’t have to implement it from scratch (which would have been extremely difficult and time-consuming). Enter html2canvas library. It renders an element onto the canvas after which we can convert it into an image. I will now explain how we implemented png export in the calendar mode. You can view the whole schedule template file here

Here is a screenshot of calendar or grid view of the schedule. Currently selected date is 18th Mar, Saturday. The PNG Export button is on the top-right corner beside the ‘Calendar View’ button.

32fa6d15-59da-4521-b348-6c01f6af7825.png

Here is a little excerpt of the basic structure of the calendar mode of the sessions. I have given an overview of it in the comments.

<div class="{{slug}} calendar">
 <!-- slug represents the currently selected date -->
 <!-- This div contains all the sessions scheduled on the selected date -->
 <div class="col-md-12 paddingzero">
   <!-- Contain content related to current date and time -->
 </div>
 <div class="calendar-content">
   <div class="times">
     <!-- This div contains the list of all the session times on the current day -->
     <!-- It is the left most column of the grid view which contains all the times →
     <div class="time">
       <!-- This div contains information about the particular time -->
     </div>
   </div>
   <div class="rooms">
     <!-- This div contains all the rooms of an event -->
     <!-- Each particular room has a set of sessions associated with it on that particular date -->
     <div class="room">
       <!-- This div contains the list of session happening in a particular room -->
       <!-- Session Details -->
     </div>
   </div>
 </div>
</div>

Now, let us see how we will actually capture an image of the HTML element shown above. Here is the code related to it:

$(".export-png").click(function() {
 if (isCalendarView === true) {

   $('.calendar').each(function() {
     if ($(this).attr('class').split(' ').indexOf('hide') <= 0) {

       $timeline = $(this);
       initialWidth = $timeline.width();
       numberOfChildElements = $timeline.find('.rooms')[0].childElementCount;
       numberOfChildElements = numberOfChildElements - 1;
       widthOfChild = $timeline.find('.room').width();
       canvasWidth = numberOfChildElements * widthOfChild + 50;
       $timeline.width(canvasWidth);
     }
   });
 }

 html2canvas($timeline, {
   onrendered: function(canvas) {
     canvas.id = "generated-canvas";
     canvas.toBlob(function(blob) {
       saveAs(blob, '' + $timeline.attr('class') + '.png');
     });
   },
 });
 $timeline.width(initialWidth);
});

Note that this initial width calculated is the width which is visible to us on the screen. In reality, the element might be scrollable and its actual width might be different. If we render the element using the initial width, we would not be able to see the full contents of that element. It will not show the whole view. Hence we need to calculate the actual width.Let us see what is going on in this code. When the user clicks on the export PNG button, we check whether we are in the calendar mode or not. If yes, then we proceed further. We then see which date is currently selected and accordingly select that div. After selecting it, we then get the initial width of that element.

So, we check all the child elements inside it, get their count and width and then calculate the actual width of the parent element based on it. Temporarily, we set this actual width as the width of the session element and pass it to the html2canvas function. That in turn, renders the whole element onto a canvas. After it has been successfully rendered onto the canvas, we save it to as an image and present a download box to the user for downloading that image.

Here is the download pop-up box

bfbd12c2-f6ed-4dae-8485-bfd1638faf74.png

And this is the downloaded PNG Image. Click on it for a higher resolution!

Screenshot from 2017-07-03 00-31-56.png

Resources

Integrating Selenium Testing in the Open Event Webapp

Enter Selenium. Selenium is a suite of tools to automate web browsers across many platforms. Using Selenium, we can control the browser and instruct it to do an ‘action’ programmatically. We can then check whether that action had the appropriate reaction and make our test cases based on this concept. There are various implementations of Selenium available in many different languages: Java, Ruby, Javascript, Python etc. As the main language used in the project is Javascript, we decided to use it.

https://www.npmjs.com/package/selenium-webdriver
https://seleniumhq.github.io/selenium/docs/api/javascript/index.html

After deciding on the framework to be used in the project, we had to find a way to integrate it in the project. We wanted to run the tests on every PR made to the repo. If it failed, the build would be stopped and it would be shown to the user. Now, the problem was that the Travis doesn’t natively support running Selenium on their virtual machine. Fortunately, we have a company called Sauce Labs which provides automated testing for the web and mobile applications. And the best part, it is totally free for open source projects. And Travis supports Sauce Labs. The details of how to connect to Sauce Labs is described in detail on this page:

https://docs.travis-ci.com/user/gui-and-headless-browsers/

Basically, we have to create an account on Sauce Labs and get a sauce_username and sauce_access_key which will be used to connect to the sauce cloud. Travis provides a sauce_connect addon which creates a tunnel which allows the Sauce browsers to easily access our application. Once the tunnel is established, the browser in the Sauce cloud can use it to access the localhost where we serve the pages of the generated sites. A little code would make it more clear at this stage:

Here is a short excerpt from the travis.yml file :-

addons:
 sauce_connect:
   username: princu7
 jwt:
   secure: FslueGK2gtPHkRANMpUlGyCGsr1jTVuaKpP+SvYUxBYh5zbz73GMq+VsqlE29IZ1ER1+xMfWuCCvg3VA7HePyN6hzoZ/t0LADureYVPur6R5ZJgqgQpBinjpytIjo2BhN3NqaNWaIJZTLDSAT76R7HuNm01=

As we can see from the code, we have installed the sauce_connect addon and then added the sauce_username and sauce_access_key which we got when we registered on the cloud. Now, what is this gibberish we are seeing? Well, that is actually the sauce_access_key. It is just in its encrypted form. Generally, it is not a good practice to show the access keys in the source code. Anyone else can then use it and can cause harm to the resources allocated to us. You can read all about encrypting environment variables and JWT (JSON Web Tokens) here:-

https://docs.travis-ci.com/user/environment-variables/
https://docs.travis-ci.com/user/jwt

So, this sets up our tunnel to the Sauce Cloud. Here is one of the screenshots showing that our tunnel is opened and tests can be run through it.

sauce.png

After this, our next step is to make our test scripts run in the Sauce Cloud through the tunnel. We already use a testing framework mocha in the project. We can easily use mocha to run our client-side tests too. Here is a link to study it in a little more detail:

http://samsaccone.com/posts/testing-with-travis-and-sauce-labs.html

This is a short excerpt of the code from the test script

describe("Running Selenium tests on Chrome Driver", function() {
 this.timeout(600000);
 var driver;
 before(function() {
   if (process.env.SAUCE_USERNAME !== undefined) {
     driver = new webdriver.Builder()
       .usingServer('http://'+    process.env.SAUCE_USERNAME+':'+process.env.SAUCE_ACCESS_KEY+'@ondemand.saucelabs.com:80/wd/hub')
       .withCapabilities({
         'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
         build: process.env.TRAVIS_BUILD_NUMBER,
         username: process.env.SAUCE_USERNAME,
         accessKey: process.env.SAUCE_ACCESS_KEY,
         browserName: "chrome"
       }).build();
   } else {
     driver = new webdriver.Builder()
       .withCapabilities({
         browserName: "chrome"
       }).build();
   }
 });

 after(function() {
   return driver.quit();
 });

 describe('Testing event page', function() {

   before(function() {
     eventPage.init(driver);
           eventPage.visit('http://localhost:[email protected]/FOSSASIASummit');
   });

   it('Checking the title of the page', function(done) {
     eventPage.getEventName().then(function(eventName) {
       assert.equal(eventName, "FOSSASIA Summit");
       done();
     });
   });
 });
});

Without going too much into the detail, I would like to offer a brief overview of what is going on. At a high level, before starting any tests, we are checking whether the test is being run in a Travis environment. If yes, then we are appropriately setting up the webdriver for it to run on the Sauce Cloud through the tunnel which we opened previously. We also specify the browser on which we would like to run, which in this care here, is Chrome.

After this preliminary setup is done, we move on to the actual tests. Currently, we only have a basic test to check whether the title of the event site generated is correct or not. We had generated FOSSASIA Summit in the earlier part of the test script. So we just run the site and check its title which should obviously be ‘FOSSASIA Summit’. If due to some error it is not the case, then an error will be thrown and the Travis build we fail. Here is the screenshot of a successful passing test:

6c0a0775-6ad4-45f9-bfa1-a8baef4e6401.png

More tests will be added over the upcoming weeks.

Resources:

Cross-Browser Issue: Dealing With an UI Problem Arising Due to Floating HTML Elements in Open Event Webapp

A few days back, I came across a rather interesting but hard to debug bug. The weird part was that it presented itself only on particular browsers like Firefox and didn’t exist on other browsers like Chrome.

In Open Event Webapp, we have collapsible session elements on track, room and schedule pages. The uncollapsed element would show you the major details like the title of the session, its type, name and a small thumbnail picture of the speaker. If the user clicks on it, then the element would collapse and more detail would pop up which would include the detailed description of the session and the speaker(s) presenting it.

Now, the problem which was occurring was that the collapsible sessions on the rooms page were behaving erratically. They were collapsing predictably but left behind a huge white space after they wound up on clicking. I have attached some screenshots to better demonstrate the problem. At first, I was confused. Because the same thing worked perfectly on Chrome.

collapsed.png

Collapsed State

uncollapsed.png

On Clicking Again

The actual solution for this problem turned out to be quite short (a one-liner in fact) but it took me a little while to solve it and actually understand what was going on. The solution:-

.room-filter {
 overflow: hidden;
}

Here, room-filter is the class applied to the session element.

Wait? But how did that solved the problem? Read on!!

As I mentioned in the title, the problem arises due to the floating HTML elements. I am just going to give a quick introduction to the float property. In case you want to read more about it, I have provided links at the end of the article which you can go through for a detailed explanation. Float is a CSS positioning property. It tells whether an element would shift to the right or left of its parent container or another floated element while other HTML elements like images, text and inline elements would wrap around it. One popular analogy which is often given is that of a textbook where the text wraps around the images. We can say that the image is floating to the left (or right) while the text wraps around it.

But how is this problem related to the floating elements still remain unclear? Hold on. This is the structure of the session div on the rooms page:

<div class="room-filter" id="{{session_id}}">

<div class="eventtime col-xs-2 col-sm-2 col-md-2">
<!-- Some Content -->
</div>

<div class="room-container">
<div class="left-border col-xs-10 col-sm-10 col-md-10">
<!-- Some Content -->
</div>
</div>

</div>

 

As we can see from the code, we have a session div with two divs inside it. We have applied bootstrap grid classes to them. These bootstrap layout classes actually make the elements float with a specified width (You can check it on the developer console). So, we have two floating div elements inside the parent div. Remember that although the floating elements remain the part of the document, they are taken outside of the normal flow of the page. Since the parent div here contain nothing but the floating elements, its height collapses to zero.

Because of this, Firefox was not able to correctly expand and collapse the session element because it’s actual height was not being computed and was set to zero. Now, if we apply any of the properties which can make the height of the parent element non-zero, we would be able to solve this problem. Chrome somehow managed to escape this issue and worked predictably.

To make the height non-zero, we apply this property:-

.room-filter {
   overflow: hidden;
}

An intuitive reason to explain why this fixes the problem is that when the element is in its default state (overflow: visible), it does not need to calculate its height to display properly. Once we set overflow: hidden, it needs to see whether the max-height or max-width has been surpassed and hence it needs to calculate its dimensions. Once the height is calculated, it is used in layout and the element no longer collapses.

correct.png

Problem solved.

Another alternative solution to solve the problem can be:

.room-filter {
   display: inline-block;
   width: 100%;
}

In case you want to read more about the float property, you can consult these links:

https://css-tricks.com/all-about-floats/

https://www.smashingmagazine.com/2009/10/the-mystery-of-css-float-property/

Using Flexbox for Responsive Layout in Open Event Webapp

Recently, I tackled the issue of alignment of different buttons and input bar in the Open Event Webapp. The major challenge was to create a responsive design which adapts well on all the platforms: desktop, tablets and mobile. Doing it using grid layout provided by Bootstrap was rather tough and complicated and I solved the problem using the flexbox.

What is Flexbox?

Flexible Boxes, also called as Flexbox, is a new layout mode introduced in CSS3. The best part of using flexbox is that it ensures that the elements behave in a predictable manner when the page layout accommodates different screen sizes and display devices. In other words, it helps us in making a responsive design in a simpler manner. It is an alternative to block elements being floated and manipulated using media queries. Using Flexbox, a container and its children can be arranged in any direction: up, down or left and right. Size is flexible and elements inside the flexbox can grow or shrink to occupy unused space or prevent overflow respectively.

The difference with grid layouts?

There is a slight difference between flexbox and grid layouts which makes one suitable for creating a fully complete layout and the other not so much.

Ideally, grids (provided by Bootstrap for example) are used for creating an entire layout. Flexbox is suited for styling separate containers such as navbars for example.

flex_terms.png

                                 Structure of Flexbox Container

How did I solve it?

First I defined a container which acted as a wrapper for all the buttons and the search bar. Now, to convert this container into an actual flexbox, we have to add display: flex property to it.

.container {
 display: flex;
}

Did the issue got solved? It doesn’t look so. The container enclosed in the red border is the flexbox.

flexnogrow.png

As we can see from the picture itself, the elements are quite close to each other and a lot of space is wasted. Fortunately, flexbox provides us a handy property called flex-grow which deals with this type of problem. It defines the ability for a flex item to grow if necessary. That it, it tells how much amount of the available space inside the flex container the item is allowed to take. If all the items have flex-grow set to 1, then the remaining space in the container would be equally distributed to all the items. In a similar way, if an item has flex-grow set to 2, then it would occupy twice the amount of available space when compared to the other items.

So, I applied the flex-grow:1 on the items.

.list-btn {
   flex-grow: 1;
}

.search-filter {
   flex-grow: 1;
}
.starred-btn {
   flex-grow: 1;
}

.calendar-btn {
   flex-grow: 1;
}

How does it look now?

flexbox_initial.png

Much better. But is the problem solved now? No. There is one more thing that we haven’t checked yet. Yes. It’s the responsiveness. We haven’t yet checked how it displays on tablets and mobiles yet? Let’s test and check and what we see.

mobilenowrap.png

Ok. Something is not right. The items are squeezing together. That doesn’t look good. We don’t want the elements to wrap on a single line. What we actually want is that the items would stack on top of each other when the screen size is reduced. That would look much more neat and tidy.

To change this default behavior of wrapping of items, we use the flex-wrap property on the container. Specifying flex-wrap: wrap does the trick and the items wrap as needed.

mobilewrap2.pngmobilewrap1.png

 

.container {
   display: flex;
   flex-wrap: wrap;
}

The result looks much better now. The items wrap on a single line only when there is enough space available. Otherwise, they wrap up onto multiple lines from top to bottom.

Flexbox is a great tool for creating custom layouts for separate containers. Apart from the properties discussed here, there are a plethora of other options which can be used to customize the behavior of flexbox and the items contained inside it.  Check out the links below for more information!

Resources:

Handling Zip Upload in Open Event Webapp

In Open Event Webapp, we use Socket.IO library for real-time communication between client and the server. To be able to generate a site for an event, the user has to first upload the file to the generator. There are a lot of node libraries like multer and formidable which exists for this purpose. But they don’t support display of real-time stats regarding the progress of the uploaded file. To solve this issue, the project used socket-io-file-upload library which uploads the file to the Node.js server using Socket.IO and show live percentage denoting how much of the zip has been uploaded to the server.

It was working quite well until we discovered a major problem with the library. It didn’t support canceling the upload. If we clicked on the cancel button, it stopped showing the progress on the front end but actually continued to upload the file to the server on the back end. We had only two choices: Either to refresh the page or just wait for the existing zip file to upload completely. The former is a really bad solution and the latter result in wastage of time and bandwidth.

On investigating the issue, the problem was with how the library is designed. This was not a fault in our code or the library either. It was just that the library didn’t support this functionality.  After thoroughly searching for the solutions, we came across this library named socket-io-stream which met our requirements. It supports bidirectional binary data transfer with Stream API through Socket.IO. We can also cancel the upload in between.

For streaming between server and client, we will send stream instances first. To receive streams, we just wrap socket with socket-io-stream and then listen for any events as usual.

Client Side:

function uploadFile(file) {
  // Calculating size of the file and creating a stream to be sent to the server

  var size = (file.size/(1024*1024)).toString().substring(0, 3);
  var stream = ss.createStream();

  /* Creating a read stream and initializing variable to keep track of the size of the file uploaded so far */

  var blobStream = ss.createBlobReadStream(file);
  var fileUploadSize = 0;

  // Tracking upload progress and updating the variables

  blobStream.on('data', function(chunk) {
    fileUploadSize += chunk.length;
    var percent = (fileUploadSize / file.size * 100);

    /* isCancelling is a boolean which stores the status of the zip upload.That is, whether it is live or canceled */

      if (isCancelling) {
           var msg = 'File upload was cancelled by user';
           stream.destroy();
           socket.emit('cancelled', msg);
           isCancelling = false;
           console.log(msg);
       }

       updateUploadProgress(Math.floor(percent));
       if (percent === 100) {
           uploadFinished = true;
           enableGenerateButton(true);
       }
     });

  // Sending the stream to the server and transferring read data to it

   ss(socket).emit('file', stream, {size: file.size, name: file.name});
   blobStream.pipe(stream);
}

Server Side:

// Importing the library
const ss = require('socket.io-stream');

// Listening to the stream sent from the client
ss(socket).on('file', function(stream, file) {
  generator.startZipUpload(socket.connId);

  // Declare a filename and write the incoming data to it
  var filename = path.join(__dirname, '..', 'uploads/connection-' + id.toString()) + '/upload.zip';

  stream.pipe(fs.createWriteStream(filename));
});

That’s all we need to do to stream data from the client to the server. Head over here for more information regarding the library.