Automated web shop testing with WebdriverIO

We’ve recently migrated our website to a new environment. While we were at it, we thought we could complement our simple uptime-Testing with some automated web shop testing, so that we can sleep at night knowing our site is OK. We’ve implemented some tests with WebdriverIO and thought we’d share the procedure, in case you’re looking for a quick guide. The entire test script is available for download here.

Our test scenario

When somebody buys Ghostlab from our website, they have to do several things: add Ghostlab to the cart, go to the cart page, click the checkout button, provide all required information in the checkout form, pay for the order, and finally they will expect to receive their license information via E-mail. And that’s exactly the procedure we want to pack into an automated test run.

  • Automated test steps
  • Go to Ghostlab landing page
  • Click “Buy” link to get to WooCommerce product page
  • Add Ghostlab to the cart
  • Click on “View cart” to see the cart contents
  • Proceed to checkout
  • Complete checkout form
  • Pay – we’ll do that with a trick
  • On order completion, receive E-mail with license key

 

Set up WebdriverIO project

Before you can get started, you have to set up a WebdriverIO project. It’s really easy, just follow the WebdriverIO guide. What you’re doing is basically:

  • set up your project directory
  • download selenium server and driver – be sure to start the server after download.
  • install webbdriverio via npm

Once you’re set up, let’s directly create a test runner for our project. Just follow the instructions for how to complete the testrunner setup dialog (WebdriverIO guide, “Let’s get serious”). Note that as of today (January 13 2017), there is one additional setup step in the dialog that is not described in the guide – I just selected the default value.

I’m assuming that you now have your test/specs directory. Now create a JavaScript file there that lets you write your test. You should end up with a setup comparable to the one in the image below.

# To run your test(s), simply execute the 
# following from the command line in the 
# project directory. 
./node_modules/.bin/wdio wdio.conf.js
Why WebdriverIO?
There are many tools to create automated tests. When we set up our tests, we were looking for something that was powerful, well-documented, easy to setup, and locally available (i.e. no SaaS). We gave several tools a quick spin, and WebdriverIO convinced us because the time to your first hello world is really short, and it is very well documented.

Now on to the actual test steps…we will only be looking at a small set of features of the Webdriver API – for the full reference, see WebdriverIO API Docs.

1: Navigate to landing page, follow to WooCommerce product page, add Ghostlab to cart

We want to start from our product landing page, so we need to point the browser to that location.

browser.url('https://www.vanamco.com/ghostlab');

On this page, there is a Buy button. We’d expect visitors who want to buy Ghostlab to click it and get to the WooCommerce product page. After clicking the button, we need to wait for the WooCommerce product page to load before we can perform our further actions. Therefore, we’re waiting for an element to be present that was not on the landing page, but is on the WooCommerce product page, namely the “Add to cart” button.

browser.click('=Buy');
browser.waitForExist('.single_add_to_cart_button.button', 5000);

Both the click and waitForExist function expect a Selector as first argument. A selector is used to specify the element in question – the one that we want to click on, or the one we’re waiting for to be present.

We’re specifying a timeout of 5000 milliseconds for waitForExist – in case the page does not load within this time, the test will fail. So make sure you specify a timeout long enough for your page to load.

Selectors
There’s a wide range of possible selectors, we’ve found the link text (‘=Buy’, on anchor elements) and CSS selectors to be easy to handle. Of course, which selectors best suit you depends on the content of your page. As soon as you start using selectors, be aware that changes in your page might break your test. For example, if we were to change the text of the buy button to “Get one now”, the test would no longer work. See the Developer Guide for a full reference of available selectors.

As soon as the WooCommerce product page has loaded, there will be an element to add the product to the cart, and our test run will continue. Next we actually want to add 1x Ghostlab to the cart by pressing the button. When this is done, the page will show a confirmation message that we’ve added it to the cart – and there will be a link that takes us to our shopping cart. So we wait for the confirmation to appear, then follow the cart link.

browser.click('.single_add_to_cart_button.button');
browser.waitForExist('.woocommerce-message > a', 5000);
browser.click('.woocommerce-message > a');

2: Enter coupon code so we don’t have to provide payment information, continue to checkout

As soon as the cart is displayed, we enter a coupon code we’ve prepared for the test that gives us 100% off. This means that our order totals 0$ and we can perform the checkout without entering payment information.  We detect whether the cart has loaded by checking for the presence of the coupon code field. We then wait for the confirmation message WooCommerce writes to the page to show that the code was applied, and verify that it was successfully applied by examining the content of the message.

browser.waitForExist('input[name="coupon_code"]');
browser.setValue('input[name="coupon_code"]', config.couponCode);
browser.click('input[name="apply_coupon"]');
browser.waitForExist('.woocommerce-message', 5000);
var msg = browser.getText('.woocommerce-message').trim();
assert(msg === 'Coupon code applied successfully.', 'Did not see coupon code applied successfully message');

Once the coupon code is applied, we go to the checkout form by clicking the proceed to checkout link. We detect the load of the checkout form by checking for the presence of the place order button – which we will use in the end to actually place our order.

browser.click('.wc-proceed-to-checkout > a');
browser.waitForExist('input[name="woocommerce_checkout_place_order"]');

3: Complete checkout form

Now we can fill out the checkout form. We’ve stored the field selectors and the associated values in a separate array over which we can iterate – of course, you could also solve this differently. One field we have to treat specially is the country selector – this is not a simple select, but a JavaScript-enhanced select component where you can search for values, then select them by clicking them. We are unable to simply set the value as with a regular input field, so we need to perform the click-search-select pattern that a real user would use.

 

config.checkoutForm = [
 {s: 'input[name="billing_first_name"]', v: 'Automatic'} /*, ... */
];

for (var i = 0; i < config.checkoutForm.length; i++) {
    var item = config.checkoutForm[i];
    browser.setValue(item.s, item.v);
 }
browser.click('.country_select');
myWait(1000);
browser.setValue('#s2id_autogen1_search', 'Switzerland');
myWait(500);
browser.click('.select2-result-label');

 

We are using the myWait function. This will simply make the test wait for a specified number of milliseconds by using the waitUntil function in combination with some time math. We use it on the JavaScript select box to make sure that the page has had the time to first render the search box, then filter down the results. We’ll later use the myWait function before trying to fetch the license e-mail.

/*
 * Simple function to wait for a specified number of milliseconds (roughly)
 */
function myWait(ms) {
    console.log('Waiting for ' + (ms/1000) + ' seconds');
    var now = new Date().getTime();
    browser.waitUntil(function(){
        return (new Date().getTime() - now) > ms;
    }, ms + 1000)
}

4: Place order, verify order confirmation, extract order number

Now that the form is completely filled out, we can place the order. For that, we simply click the place order button. After the order has been processed, we will see a confirmation page. We verify that we have reached it by waiting for the thank you note, and by examining the content of the message. Also, we extract the order number assigned to our order by the system. We need this to check if we have received the license e-mail.

browser.click('input[name="woocommerce_checkout_place_order"]');
browser.waitForExist('.woocommerce-thankyou-order-received');
msg = browser.getText('.woocommerce-thankyou-order-received').trim();
assert(msg === 'Thank you. Your order has been received.', 'Did not see order has been received message.');

console.log('Extracting order number...');
var orderNumber = browser.getText('.woocommerce-thankyou-order-details.order_details > li:first-child > strong').trim();

 

5: Verify that license e-mail has been sent

On order placement, our backend should have generated a license key for Ghostlab and sent it to the e-mail address we have provided in the checkout form. In order to complete the test, we need to see if that e-mail has actually been delivered. For that, we use a node imap package to connect to the e-mail account we provided on purchase. Before we try and fetch the e-mail, we wait for 30 seconds, otherwise, it might not have been delivered yet.

Mocha timeouts
By default, mocha (which we’re using in our example) has a timeout of 10s – so when we would wait for 30s, that would fail, as the global timeout would apply after 10s. You can increase the mocha timeout in the mocha options (mochaOpts.timeout), this can be found in the file wdio.conf.js, which is automatically created for your project.

We know that the Subject text of that license e-mail is unique – it is some static text combined with the order number (that’s the unique part). So we can simply search for a message with the expected subject, and will be satisfied if the number of messages returned by the search is exactly 1. Of course, we could go on to examine the content of the license email, maybe even try to activate the software product with the license – but we’ll leave that for some other time.

// Let's wait a bit before we try to fetch the E-mail
myWait(10 * 1000);

var subject = 'Ghostlab 2 license for your order ' + orderNumber + ' (1 x Ghostlab)'
var foundEmail = false;
imap.once('ready', function(){
  imap.openBox('INBOX', true, function(err, box){
    imap.search([ 'UNSEEN', ['SUBJECT', subject] ], function(err, results) {
      if (err) 
        throw err;
       var resultsLength = results.length;
       assert(resultsLength === 1, 'Did not find exactly one e-mail with license information');
       foundEmail = true;
      imap.end();
    }); 
  });
});
imap.connect();

// Wait for e-mail to be confirmed.
browser.waitUntil(function() { return foundEmail; }, 10000);

Note that the call to imap.connect() is asynchronous – so how do we “notify” our test that e-mail fetching has been successful? We use the waitUntil function and pass it a function that returns a variable. In the e-mail handling, we set the variable to true if we successfully fetch the e-mail.

The entire test script is available for download here.

Ghostlab 2 is here

We’re excited to announce that Ghostlab 2 is now available! Let us give you a quick tour of what’s new, and what’s the same.

As in version one, Ghostlab 2 focuses on helping you making responsive and multi-device testing much faster and more convenient. It lets you browse any site you’re interested in, keeping any number of connected clients in sync by propagating scrolling, clicking, and any other site interactions it discovers. This works for local browsers and mobile devices on your network. It lets you (validly!) fill out forms automatically within a fraction of a second, lets you keep track of all connected clients, and offers you the ability to identify problems and fix them on any of them.

At the same time, we’ve introduced many changes in version 2 – some of them significant, some of them more on the minor-but-useful side. We’d like to present three of them that we’re particularly happy to share with you.

ghostlab interface

Client inspection

In Ghostlab 1, we’ve included Weinre, an awesome project that allowed us to remotely inspect the DOM on any connected device. However, we wanted to do more than that, and decided we’d have to rethink how we provide remote inspection tools. In Ghostlab 2, we are including the developer tools you know from Google Chrome – with some tweaks of our own. You are still able to inspect any connected client – but in addition, you can now inspect all of them synchronously.

That means that when you remotely “fix” that CSS property that destroys your page in Chrome or Firefox, you’ll immediately also see what the consequences are for any other connected browser or device.

On top of that, we decided that inspecting CSS is often not enough – especially after some bad experiences finding JavaScript bugs on mobile phones. So we’ve come up with a solution that lets you remotely debug JavaScript on any client that is connected to Ghostlab – be it a browser, a tablet, a mobile phone or even a TV set. We’ve even been told it works on IE8!

ghostlab inspect debug Javascript

Preprocessors

To allow Ghostlab to further integrate into the workflow, we’ve also added the ability to compile several languages. Initially, we’re shipping support for Sass / Less / Stylus, Jade / Haml, and CoffeeScript / Typescript. When you’re using Ghostlab with your local site, it will now be able to compile all these source files into browser-ready HTML/CSS/JS and then automatically refresh the page on your devices.

We’re not including binary compiler packages in our distribution, rather, Ghostlab will attempt to install them them should they not be present on your system. We’re using npm and gem for that purpose. This makes it easy to offer a wide range of preprocessors in the future while not making the Ghostlab distribution too heavy. While the initial set of precompilers may seem limited, we’ll constantly be on the lookout for interesting candidates to include.

ghostlab compile scss compile less

Redirection service

We’ve been told by several customers that when working in a team, pointing mobile devices to the Ghostlab server can be cumbersome. Of course, in order to avoid having to enter the URL on every one of them each time (which is slow even using the provided QR Code), they had bookmarked the Ghostlab server URL on every device. but: which URL? Several developers means several computers means several IP addresses – so each developer has a different (and potentially changing) IP address and the device bookmark will ever so often point to the wrong URL.

The idea of the redirection service is simple. A redirection bookmark is just a URL, looking something like http://302.pm/ABC677. It redirects to an IP address in your local network, like http://192.168.2.3:8005. The redirection bookmark has an access token, and using that access token, Ghostlab is able to update the redirection bookmark to the current IP and port where Ghostlab runs. When on a device, you access the bookmark, you will always be redirected to the up-to-date Ghostlab address. This way, you can either share redirection bookmarks within a team (by sharing the access token), or simply create your individual and permanent Ghostlab URL for your devices to use.

Enough said. Give it a spin!

So much from us. You can have a look at the Ghostlab website, where you can download a full-featured and completely free 7-day trial!

IndexedDB fundamentals – Plus a indexeddb example – tutorial

IndexedDB is a relatively fresh NOSQL database implemented in modern browsers. In this blog post I’m going to show you how to perform some basic operations with it. The tutorial has been optimised for use with Google’s latest Chrome browser.

In Chrome’s developer tools, IndexedDB can be found in the Resources tab. For the moment, any changes to your IndexedDB databases aren’t automatically visible in the developer tools – you have to either refresh the page or right-click on “IndexedDB” in the Resources tab and choose “Refresh IndexedDB”.

After two short general remarks, we’ll dive right into the code!

Limits
IndexedDB has no hard storage limits on it’s own. However, browser vendors have soft limits. Firefox will ask for permission to store blobs bigger than 50 MB. Google Chrome has various limits for different use cases, for more information about Chrome limits see https://developers.google.com/chrome/whitepapers/storage

Fallback
For older browsers that don’t support IndexedDB, you can use IndexedDBShim. It allows WebSQL (which is no longer developed) to handle IndexedDB API calls when they are not present in the browser. Available at https://github.com/axemclion/IndexedDBShim.

Creating, dropping and retrieving databases

Firefox does not prefix IndexedDB, Chrome had prefixed it for a while but now no longer does it, and Internet Explorer uses an MS vendor prefix. To achieve compatibility across browsers, you might want to use the declaration solution below.

[javascript]
var indexedDB = window.indexedDB || window.webkitIndexedDB || window.msIndexedDB;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
var openCopy = indexedDB && indexedDB.open;

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

if (IDBTransaction)
{
IDBTransaction.READ_WRITE = IDBTransaction.READ_WRITE || ‘readwrite’;
IDBTransaction.READ_ONLY = IDBTransaction.READ_ONLY || ‘readonly’;
}
[/javascript]

Remember IndexedDB is a NoSQL database, so it doesn’t contain tables, but object stores. Object stores contain data in the form <key, value> – where the value can be a simple string or a complex object. To create your first database, simply attempt to open it. Since it doesn’t exist yet, the upgradeneeded event will be triggered – which you handle in order to create your object store(s). Notice that whenever you want to modify your databases object store, you need to do this by increasing the version number of your database – the version upgrade will again trigger the upgradeneeded event, which you can handle to make the necessary modifications. To open a database indicating that you want to create a new version, simply add the version number as the second parameters to your indexedDb.open() call. Dropping a database is straightforward.

[javascript]
/***
* Create database snippet
* */
var request = indexedDB.open(‘todos’);

request.onupgradeneeded = function(e)
{
// e is an instance of IDBVersionChangeEvent
var idb = e.target.result;

if (idb.objectStoreNames.contains(‘todo’))
{
idb.deleteObjectStore(‘todo’);
}

var store = idb.createObjectStore(‘todo’, {keyPath: ‘text’, autoIncrement: true});
// createIndex operations possible to be pefromed on store.createIndex
store.createIndex(‘by_todo’, ‘todo’, {unique: true, multiEntry: false});
};

request.onsuccess = function(e) { /* add, update, delete, … */ };
request.onerror = function(e) { /* handle error */ };

/***
* Remove database snippet
* */
var dropDatabase = function(name)
{
var request = indexedDB.deleteDatabase(name);
request.onsuccess = function() { /* drop succeeded */ };
request.onerror = function() { /* drop failed */ };
};
[/javascript]

If you want to programatically find out which databases are available, you are currently limited to webkit (other browsers don’t offer such a method). Note that IndexedDB follows a same-origin policy – so you won’t be able to read databases from domains other than your own.

[javascript]
indexedDB.webkitGetDatabaseNames().onsuccess = function(e)
{
var databaseNames = [];
for (var i = 0, l = e.target.result.length; i < l; i++)
databaseNames.push(e.target.result[i]);
};
[/javascript]

Creating records

Adding a record to object store requires creating a “readwrite” transaction, as demonstrated below. If your browser supports IndexedDB, you can populate a database with a todo object store and some example entries by clicking the button below the code sample. Make sure you have your dev tools open so that you can see what is going on on the database.

[javascript]
var request = indexedDB.open(‘todos’);

request.onsuccess = function(e)
{
var idb = e.target.result;
var trans = idb.transaction(‘todo’, IDBTransaction.READ_WRITE);
var store = trans.objectStore(‘todo’);

// add
var requestAdd = store.add({text: ‘Go to Coop’, todo: ‘Groceries’});

requestAdd.onsuccess = function(e) {
// do something
};

requestAdd.onfailure = function(e) {
// failed
};
};
[/javascript]

Retrieving records

You can retrieve records in a read-only transaction. For iterating over all entries in an object store, simply use the openCursor function. If you have previously chosen to add the six predefined todo entries to the demo store, you can retrieve them using the button below the code.

[javascript]
var request = indexedDB.open(‘todos’);
request.onsuccess = function(e)
{
idb = e.target.result;
var transaction = idb.transaction(‘todo’, IDBTransaction.READ_ONLY);
var objectStore = transaction.objectStore(‘todo’);

objectStore.openCursor().onsuccess = function(event)
{
var cursor = event.target.result;
if (cursor)
{
console.log(‘Cursor data’, cursor.value);
cursor.continue();
}
else
{
console.log(‘Entries all displayed.’);
}
};
};
[/javascript]

When opening a cursor to the object store, you can specify various parameters for limiting the scope of the cursor. The following examples demonstrate several of these constraints. Again, if you have chosen to use the build-in demo via the buttons, you can continue to do so for every code example.

The interface we are using to apply constraints to our cursor is IDBKeyRange. When specifying bounds, you can indicate if you want them to be open (excluding the endpoint values, pass true) or closed (including the endpoint values, pass false – this is the default). For in-depth information refer to the documentation on the Mozilla Developer Network.

[javascript]

// Retrieve all records after and including ‘Groceries’ key
var cursor = IDBKeyRange.lowerBound(‘Groceries’, false);
objectStore.openCursor(cursor).onsuccess = function(event)

[/javascript]

[javascript]

// Retrieve all records up to and including ‘Groceries’ key
var cursor = IDBKeyRange.upperBound(‘Groceries’, false);
objectStore.openCursor(cursor).onsuccess = function(event)

[/javascript]

[javascript]

// Retrieve all records between ‘Bills’ and ‘Flat’ keys (inclusive)
var cursor = IDBKeyRange.bound(‘Bills’, ‘Flat’, false, false);
objectStore.openCursor(cursor).onsuccess = function(event)

[/javascript]

[javascript]

// Retrieve only the record matching the ‘FeedTheDog’ key
var cursor = IDBKeyRange.only(‘FeedTheDog’);
objectStore.openCursor(cursor).onsuccess = function(event)

[/javascript]

 

Editing data

To edit or delete records on a database, you will need to create a read-write transaction. The following examples show you how you can edit a record (basically, retrieve it and update it via the object store’s put method) and delete a record (calling delete on the object store and passing the key). If you have populated the demo database using the buttons before, you can now edit and delete these records using the buttons below the code.

[javascript]
// Editing a record
var editRecord = function(key, newValue) {
var request = indexedDB.open(‘todos’);
request.onsuccess = function(e)
{
var idb = e.target.result;
var objectStore = idb.transaction(‘todo’, IDBTransaction.READ_WRITE).objectStore(‘todo’);
var request = objectStore.get(key);

request.onsuccess = function(ev)
{
var data = ev.target.result;
var editDivEl = document.querySelector(‘#editRecordDiv’);

if (data === undefined)
{
editDivEl.innerHTML = ‘Key doesnt exist or has been previously’ +
‘removed’;
return;
}

data.text = newValue;
var result = objectStore.put(data);

result.onsuccess = function(ev)
{
var todoName = ev.target.result;
editDivEl.innerHTML = ‘Successfully edited key <b>’ +
todoName + ‘</b>’;
};

result.onerror = function(ev)
{
console.log(‘Error occured’, ev.srcElement.error.message);
};
};

request.onerror = function(ev)
{
console.log(‘Error occured’, ev.srcElement.error.message);
};
};
};

var reminderDate = new Date();
editRecord(‘Bills’, ‘Reminder ‘ + reminderDate.getHours() + ‘:’ + reminderDate.getMinutes() +
‘:’ + reminderDate.getSeconds());
[/javascript]

[javascript]
// Deleting a record
var request = indexedDB.open(‘todos’);
request.onsuccess = function(e)
{
var idb = e.target.result;
var objectStore = idb.transaction(‘todo’, IDBTransaction.READ_WRITE).objectStore(‘todo’);
var request = objectStore.delete(‘Bills’);

request.onsuccess = function(ev)
{
console.log(ev);
};

request.onerror = function(ev)
{
console.log(‘Error occured’, ev.srcElement.error.message);
};
};
[/javascript]

Limiting queries

As a final example in this tutorial, I want to show you a way of limiting the number of results returned by a query. IndexedDB doesn’t provide a straightforward method for limiting and grouping database results. Therefore, I have created a simple helper function that helps us limiting the number of records retrieved by a query. Note that in such a context, the IDBCursor.advance method might come in handy, as it lets you skip retrieved records. In the example below, we abort the transaction once the indicated limit has been reached.

[javascript]
/***
* IndexedDB limit output syntax similar to LIMIT 10, 5;
* */
var limitRecords = function(pageSize, skipCount)
{
var request = indexedDB.open(‘todos’);

request.onsuccess = function(e)
{
idb = e.target.result;
var transaction = idb.transaction(‘todo’, IDBTransaction.READ_ONLY);
var objectStore = transaction.objectStore(‘todo’);
var idx = 0;

objectStore.openCursor().onsuccess = function(e)
{
var cursor = e.target.result;
if (cursor)
{
if (skipCount <= idx && idx < pageSize + skipCount)
console.log(‘Cursor is in the range ‘, cursor);

idx++;

if (idx >= pageSize + skipCount + 1)
{
// we have all data we requested
// abort the transaction
transaction.abort();
} else {
// continue iteration
cursor.continue();
}
}

};
};
};
[/javascript]

Done.

We hope you’ve enjoyed this little tutorial! Of course, there is much more to IndexedDB than what we have covered here – this was just to get you started!








Ghostlab & Unic browser testing, behind the scenes

We are proud to work with Unic

It is always great to know a client is happy using our products to do browser testing, and Unic is a very special one.

Using Ghostlab has made the iteration between Unic front-end engineers and designers more efficient and to prove that, the guys at Unic created a great video to showcase how Ghostlab has uniquely created a more effective workflow. Check it out below.

Making of Unic Device Lab from Unic on Vimeo.

Browser testing – who is Unic?

Unic is a leading provider of high-quality e-business solutions in Europe. With solutions for communication, sales and cooperation, we increase our customers’ success. Unic provides integrated solutions from one source: we combine competencies in consulting, creation, implementation and operation. Unic solutions create measurable added value, quality and stability. As an owner-managed group, Unic relies on independence in the strategic and operating management. Unic has been successful in the market since 1996 and is today represented in three countries with 260 employees.

We evaluated Software for our Device Lab. The Ghostlab developer team was quick to understand our specific issues, and was able to present us with solutions expertly and constructively at all times.

We hope that this story has inspired you and we want to thank Unic and wish to always collaborate with them on their exciting new projects!

If you guys want to check their work, you can head over to Unic

The Ghostlab team

Geek Quiz – How much of a geek are you?

geek quiz

Geek Quiz.

We dare you to take this quiz to find out how much of a geek you are. If you do well you could win a free Ghostlab licence!

10 questions to test your knowledge of front end development.

Don’t forget to input your email at the end to be in with a chance of winning a Ghostlab licence!

If you like the geek quiz, please share it with your fellow geeks.

The chance to win a Ghostlab licence is gone but the epic geek test is still here.  Leave your email address behind for epic news and resources and quality content.

[polldaddy type=”iframe” survey=”0D05C362FA50CC85″ height=”auto” domain=”ghostlab” id=”0D05C362FA50CC85″]

 

 

Responsive Design: Envato Browser Testing Case Study

responsive-design-envato-browser-testing-case-study

Jordan Lewis (@jordanlewiz) is a frontend developer working for Envato on the Marketplaces team as delivery lead on the Purchase team.

jordan-profile


Headquartered in Melbourne, Australia, Envato is an online company with an ecosystem of sites and services to help people get creative. On our Marketplaces thousands of people earn a living selling things like WordPress themes, photos, music tracks and illustrations. We also run an education platform called Tuts+, and a freelance marketplace called Envato Studio.

The Envato Marketplaces are a collection of eight sites running from the one codebase, which makes cross-browser testing complicated. Having the right tools and workflow for testing is incredibly important for us to be confident we are delivering a quality experience to all our users.

Over a year ago we started the long journey in making the Marketplaces all responsive designs and during this time we have been using Ghostlab to help ease the pain of testing our multiple sites across the large range of browsers and devices we support. Not only do we use it for mobile device testing, but also regular desktop cross-browser testing over many operating systems running inside virtual machines.

Prior to Ghostlab we used a combination of Pow and xip.io on our development machines to serve our Marketplaces application to our testing devices and virtual machines. However, with the added complexity of debugging Javascript and CSS on mobile devices, it meant our old testing methods were incomplete.

Ghostlab quickly became an invaluable tool in our cross-browser and device testing workflow.

envato-device-lab

Due to the nature of our Marketplaces application, we want to focus our testing on performing the same actions across a range of browsers whilst being signed-in on each and simulating different scenarios. This can be from a simple “how does this button look?” to “how is the new purchase flow experience on a mobile device compared to the desktop experience?”.

ghostlab-envato

On the Marketplaces we embrace progressive enhancement and develop first for basic functionality and subsequently layer additional features on top for the browsers and devices which can support them. This often means we build a feature to work without Javascript and then introduce Ajax, modals, visual flourishes and so on to compliment the experience. As a result this makes the testing experience different across the range of browsers we support. Our strategy has been to test on one browser at a time to make sure we are happy with the experience rather than rely on the Ghostlab synchronized feature which we find can interfere with Ajax form submissions.

The Marketplaces team is growing rapidly and we are currently working on many new and exciting features from the 2014 roadmap. We are paying particular attention to improving on our user experience on mobile and tablet devices as our mobile traffic increases.

Ghostlab has removed the pain and frustration from what is arguably the most unpleasant stage of developing a website. What was once a very complex solution for only the brave developer is now as simple as scanning a QR code.