Automating JavaScript tests with QUnit, PhantomJS and Grunt

QUnit’s setup is fairly simple and is easy to use. To get started, I will be using a simple boilerplate webpage. Notice that the two files required for QUnit (qunit-x.js and qunit-x.css) are included in the page source. Additionally, I have some inline JavaScript for which QUnit tests will be written.

home.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Homepage</title>
  <link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.17.1.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>
  <script src="//code.jquery.com/qunit/qunit-1.17.1.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
  <script>
  var app_home = (function() {
    function getData() {
      var obj = {
        user_msg:'Welcome to the homepage',
        page_heading:'Homepage heading'
      };
      return obj;
    }

    return {
      getData:getData
    };
    })();

    // app logic
    jQuery(document).ready(function() {
      app_home.getData();
    });
  </script>
  <script src="tests.js"></script>
</body>
</html>

Within tests.js, I have included two basis JavaScript tests. The first test is to verify that the data within app_home.getData() matches the expectations and the second test verifies the page title. Note that QUnit enables you to group tests into modules using QUnit.module().

tests.js

// Homepage
if(typeof app_home !== 'undefined') {
	QUnit.module("home");
	QUnit.test( "homepage data", function( assert ) {
	  var actual_obj = {
				user_msg:app_home.getData().user_msg,
				page_heading:app_home.getData().page_heading
			};
	  var expected_obj = {
				user_msg:'Welcome to the homepage',
				page_heading:'Homepage heading'
			}; 
	  assert.deepEqual( actual_obj, expected_obj, "Homepage data should match expected data");
	});
	QUnit.test( "page title", function( assert ) {
  		assert.equal( document.title, "Homepage", "page title should match expectation" );
	});
}

Opening home.html in a web browser should display the QUnit test results.

qunit test

As a website or app scales, running these tests manually within a web browser can be time consuming.

Automating JavaScript tests using Grunt and PhantomJS

For the setup, first install nodejs followed by Grunt Cli
sudo apt-get install nodejs
sudo npm install -g grunt-cli

Within the root directory of your project, create a package.json file with the following content.

package.json

{
  "name": "yourProjectName",
  "version": "0.0.1",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-qunit": "~0.5.2"
  }
}

Subsequently, run npm install. This should install grunt and grunt-contrib-qunit packages within your project. You can view the contents of these packages within the node_modules directory.

npm install

The next step is to create a Gruntfile.js file within the root directory of your project with the following content. Note that Gruntfile allows command line parameters and web page specific settings to be passed to PhantomJS.
I have illustrated a few of these options in the Gruntfile below. For instance, “–cookies-file” property can be used to write cookie data to a text file. Similarly, under page specific settings – options like “resourceTimeout”, “userAgent” etc., can be set.

Gruntfile.js

module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    qunit: { 
      options: {
      	'--ignore-ssl-errors':true,
      	'--max-disk-cache-size':100000,
      	'--load-images':false,
      	'--local-to-remote-url-access':true,
      	'--ssl-protocol':'any',
        '--cookies-file':'cookies.txt'
      },
      all: {
      	options: {
      		urls: [
      			'http://test.local/home.html?module=home'
      		],
      		page: {
      			settings: {
      				resourceTimeout:10000,
      				//userAgent:'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3',
      				loadImages:false,
      				localToRemoteUrlAccessEnabled:true,
      			}
      		}
      	}
      }
    }
    
  });  
	grunt.loadNpmTasks('grunt-contrib-qunit');
	grunt.registerTask('default', ['qunit']);
};

Please go through the PhantomJS API for the full list of options.

Specifying the user agent for PhantomJS

Notice that I have commented out the userAgent property in Gruntfile – which is set to iPhone’s useragent. Uncommenting this line allows you to run the same set of JavaScript tests within PhantomJS with the useragent set to iPhone.

grunt qunit task runner

Lastly, the grunt command can be run with arguments like --verbose to see additional information and --debug to review each HTTP(S) request in PhantomJS and the sequence of QUnit steps.

Customizing Magento email templates

Magento comes with a list of default email templates for various scenarios like orders, shipments, customer interactions etc., If you would like to customize email templates in Magento whether its modifying the HTML/plain text within a template or adding dynamic content blocks, below are few handy notes.

Email templates location

The default directory where email templates are located in Magento is at app/locale/en_US/template/email/. Note that these templates are not intended to be customized via the code base.

Overriding default email templates

In order to customize an email template, navigate to Magento Admin > System > Transactional Emails. Click on Add New Template and in the following page, pick the email template that you are wanting to customize, select an appropriate locale and click on Load Template.

new email template magento
Creating a new email template from Magento Admin

This should populate the template content and template styling sections. Customize the template’s content and styling to suit your needs. Subsequently, navigate to System > Configuration > Customers > Customer Configuration > Create New Account Options (tab) > Default Welcome email.

custom welcome email template magento
custom welcome email template magento

Pick the newly created custom email template from the dropdown and Save Config. Moving forward, this customized email template would be used instead of the default template in app/locale directory.

Note: it may seem logical to copy a email template from the app/locale directory to app/design/frontend/<package>/<theme>/locale/en_US/template/email/—- and expect it to work as an override for the default template; unfortunately, that’s not the case.

System variables

There are a wide range of system variables available for use in email templates. These variables are replaced by the corresponding dynamic content; very handy to avoid redundant text across email templates.

Examples include:

  • store web address: {{store url=""}}
  • store name: {{var store.getFrontendName()}}

Some of the system variables are global like the above two examples whereas others are specific to an email template. For instance, review the order confirmation email template:

app/locale/en_US/template/email/sales/order_new_guest.html

Notice the first few lines of this file with the vars annotation. Variables like {{var order.increment_id}} are readily available for use within the transnational email templates.

Please find the list of these system variables available in this knowledge base entry.

Custom variables

Magento enables you to use custom text or HTML via custom variables. To create a custom variable, navigate to Magento Admin > System > Custom Variables.

Let’s say that you have created a simple custom variable with text “happy holidays”.  To insert a custom variable within a email template, open an email template and click on Insert Variable. Notice “holiday-message” under Custom Variables. This gives you the ability to easily swap text of HTML across multiple email templates.

Inserting custom variable in Magento email templates
Inserting custom variable in Magento email templates

Adding dynamic content

Let’s say that when a customer registers on the site, apart from using text/HTML in the welcome email, you would like to add dynamic content about your site’s latest product offerings: for instance, let’s add three latest products of type “simple” from the site to the welcome email sent out upon customer registration.

One way to achieve this is to create a custom module with a block and call this block type with the email template.

The first step is to create the bootstrap file for the custom module.
app/etc/modules/Sri_Custom.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Sri_Custom>
            <active>true</active>
            <codePool>local</codePool>
        </Sri_Custom>
    </modules>
</config>

Next, create the config.xml file for this custom module.
app/code/local/Sri/Custom/etc/config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Sri_Custom>
            <version>0.1.0</version>
        </Sri_Custom>
    </modules>
    <global>
        <blocks>
            <custom>
                <class>Sri_Custom_Block</class>
            </custom>
        </blocks>
    </global>
</config>

Next, let’s create a block with a function which returns the product collection we need for this case.
app/code/local/Sri/Custom/Block/Latestproducts.php

<?php
class Sri_Custom_Block_Latestproducts extends Mage_Core_Block_Template
{
    public function getLatestProducts()
    {
    	$collection = Mage::getResourceModel('catalog/product_collection');
		$collection->addAttributeToSort('entity_id', 'DESC')
		->addAttributeToFilter('type_id', 'simple')
		->getSelect()->limit(3);

		return $collection;
    }   
}

app/design/frontend/package/theme/template/email/latestproducts.phtml
Lastly, the template file which generates the HTML to display the product information within the email template.

<h3>Checkout our latest products</h3>
<ul>
<?php
foreach($this->getLatestProducts() as $item) {
	$product = Mage::getModel('catalog/product')->load($item->getEntityId());
	echo '<li><a href="' . $product->getProductUrl() . '">' . $product->getName() . '</a></li>';
} ?>
</ul>

This custom block can be used within the email template as

{{block type="custom/latestproducts" area="frontend" template="email/latestproducts.phtml"}}

The result would be something like,

custom email template with latest products
custom email template with latest products

Switching content blocks based on the store

In cases where an email template is used across multiple stores, there can be specific content blocks within the email template that depend on the store. For instance, in the customer welcome email template, the welcome message could be specific to the store like “Welcome to store one” / “Thanks for registering on store two”.

AppEmulation can be used to set the appropriate store environment. Below is an example from Magento core of it’s usage:

app/code/core/Mage/Sales/Model/Order.php
public function sendNewOrderEmail() {
.....
// Start store emulation process
        $appEmulation = Mage::getSingleton('core/app_emulation');
        $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($storeId);
......
// Stop store emulation process
        $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo);
}

Using Workflowy as a Agile/Scrum tool, for personal use

I enjoy the Agile Scrum project management workflow at my workplace where we use Atlassian’s Jira & Scrum tools for managing various projects. While, one may not need all the hefty features of a enterprise tool for managing personal projects, it would be nice to have a free tool with support for such a Agile Scrum workflow.

Some of the features that I expect in a Agile Scrum tool (for personal use) are:

  • Having the ability to create and manage sprints and a backlog
  • Usage of epics to categorize the list of items to be completed in a project
  • Being able to assign story points to stories
  • View progress in a sprint – with story points as a metric
  • Being able to mark items as done

Trello has been referenced for use as a scrum tool, which may need a few tweaks/hacks. While, this may work for others, I was looking for a much simpler alternative like Workflowy. In case, you haven’t heard about it before:

WorkFlowy is an organizational tool that makes life easier. It can help you organize personal to-dos, collaborate on large team projects, take notes, write research papers, keep a journal, plan a wedding, and much more.

For my wishlist of Agile/Scrum features mentioned above for personal use, here is how Workflowy can be set up:

  • Workflowy supports tagging so, tags can be used as epics for categorization
  • (hack) Use tags with a specific prefix for assigning story points to individual items
  • View progress in a given sprint using a bookmarklet

As indicated in the screenshot below, I am using  “SP-” as a standard prefix for assigning story points to individual items.

The JavaScript snippet below looks for tags starting with “SP-” on the page and displays a alert box indicating the story points for total, pending and completed items.

To use this in workflowy, simple create a bookmarklet in your browser with any name of your choice and the minified version of the code below as the URL. To see the progress in a given sprint or a view, simply click on the bookmarklet and it will display the progress.

Note: when using this bookmarklet, please make sure to have “Completed:visible” in workflowy (top-right corner). You may change this back to “Completed:hidden” after viewing the progress.

javascript: (function() {
    var allTagText = jQuery('.contentTagText:contains("SP")').text();
    var doneTagText = jQuery('.done .contentTagText:contains("SP")').text();
    var numberPattern = /d+/g;
    var allSprintStoryPoints = allTagText.match(numberPattern);
    var allCount = 0;var doneCount = 0;
    for (var i = 0; i &lt; allSprintStoryPoints.length; i++) {
        allCount += parseInt(allSprintStoryPoints[i])
    };
    var doneSprintStoryPoints = doneTagText.match(numberPattern);
  if(doneTagText.length &gt; 0) {    
    for (var i = 0; i &lt; doneSprintStoryPoints.length; i++) {
        doneCount += parseInt(doneSprintStoryPoints[i])
    };
  } else {
    doneCount = 0;
  }
  
    alert('Total: ' + allCount + '  ' + ' Pending: ' + 
          (parseInt(allCount) - parseInt(doneCount)) + 
          '  Completed:  ' + doneCount);
}());

Minified version of this code is available here.