Testing DOM Events Using jQuery and Jasmine 2.0

By Rob Gravelle

A lot of processes within a Web application are triggered by Document Object Model (DOM) events. These include button clicks, setting the focus on an element, document loading, unloading, and any other event resulting from a user action or the browser. That's why any good Unit Testing Framework will provide methods to deal with DOM events. Jasmine is no exception. It makes it possible to test that a given event occurred and what the results of the event were. In today's article, we're going to create some tests for the button click event using Jasmine 2.0, jQuery, and an additional third-party library.

What You'll Need

The add-on library that we'll be using is built on Jasmine 2.0 and jQuery and is called jasmine-jquery. Here are the details on it and the other two libraries:

  • Jasmine 2.0: In this new major release, the makers of Jasmine have dropped the runs() and waitsFor() methods in favor of the Mocha done() callback, which has been added to beforeEach(), it() and afterEach(). If you plan on performing any tests on asynchronous processes, then this version is a must.
  • Jasmine-jQuery: There are a lot of third party libraries that enhance Jasmine's basic functionality. The one we'll be using here, jasmine-jquery, adds a set of custom matchers for jQuery as well as an API for creating and managing HTML, CSS, and JSON fixtures in your specs. All of the functionality is contained in the jasmine-jquery.js file so all you need to do is include it with your other JS files and reference it from your SpecRunner.html file using a <SCRIPT> tag with the SRC attribute.
  • jQuery v1.7+: Version 1.7 is where jQuery added the on() event handler. Without it, the jasmine-jquery script will raise errors. It should work with newer versions as well. Just don't go back to any versions previous to 1.7 and you should not encounter any issues.

Setting Up Your Tests

The tests go in an HTML file called the Spec Runner. In it, you reference the required libraries and place a local script in the document that will run your tests. In the following sample file the global setFixtures() method creates the DOM from an HTML source string. Typically, your target code will be contained in existing files. These can be loaded using the loadFixtures() method. Unlike setFixtures() (seen below), it takes a URL as an argument, rather than a string:

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Jasmine v2.0 DOM Tests Spec Runner</title>

  <link rel="shortcut icon" type="image/png" href="images/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-core/jasmine.css">

  <script type="text/javascript" src="lib/jasmine-core/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jasmine-html.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/boot.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jquery-1.7.2.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jasmine-jquery.js"></script>
  
  <script type="text/javascript">
   var MSG = "Hello World!";
     
   function hideMessage() {
     $( "#pMsg" ).html("");
   }
	
   function showMessage() {
     $( "#pMsg" ).html(MSG); 
   }
	 
   function setUpHTMLFixture() {
     setFixtures('<form id="testForm" action="">'
                +'  <h1>Test Form</h1>'
                +'  <input type="text" id="txtMessage">'
                +'  <br>'
                +'  <button id="btnHideMessage" type="button" onclick="hideMessage()">Hide Message</button>'
                +'  <button id="btnShowMessage" type="button" onclick="showMessage()">Show Message</button>'
                +'  <br>'
                +'  <p id="pMsg"></p>'
                +'</form>');
      
   }
  </script>
</head>
<body>
</body>
</html>

The HTML fixture above is comprised of a form with a text field, a button that displays the contents of the text field in a paragraph and another button that clears the message.

Testing That A DOM Event Was Fired

Jasmine utilizes spies much like JUnit does Mocks, except that the former does not pretend to be a different object type. Rather, Jasmine spies simply listen for a given event and report whether or not it was triggered and, if so, by what element.

A spy is instantiated by the spyOnEvent() method. It accepts a selector string and the name of the event, sans the "on". We can then verify that the given event was called by invoking the toHaveBeenTriggered() method on expects(spy). It is also possible to check that the event was triggered on the correct element by applying toHaveBeenTriggeredOn() to expect(), passing in the element selector:

describe("Button Click Event Tests", function() {
  var spyEvent;
  
  beforeEach(function() {
    setUpHTMLFixture();
  });
     
  it ("should invoke the btnShowMessage click event.", function() {
    spyEvent = spyOnEvent('#btnShowMessage', 'click');
    $('#btnShowMessage').trigger( "click" );
      
    expect('click').toHaveBeenTriggeredOn('#btnShowMessage');
    expect(spyEvent).toHaveBeenTriggered();
  });
     
  it ("should invoke the btnHideMessage click event.", function() {
    spyEvent = spyOnEvent('#btnHideMessage', 'click');
    $('#btnHideMessage').trigger( "click" );
      
    expect('click').toHaveBeenTriggeredOn('#btnHideMessage');
    expect(spyEvent).toHaveBeenTriggered();
  });
});

Verifying the Results of an Action

Knowing that an event was triggered by the correct element a fine start, but ultimately, we're interested in the result of the action. Did it perform as expected or blow up? That's where jasmine-jquery's numerous jQuery matchers come into play. In our case, we want to know if the pMsg paragraph contains our message when the Show Message button is clicked and if it's cleared when the Hide Message button is clicked. Like the spy checkers above, the jQuery matchers are also members of the expect() return object. One that we can use is toHaveText(). It compares the element's text to the value passed in to the function. In the example below, the Show Message and Hide Message events are separated into two describe blocks:

var MSG = "Hello World!";
describe("Show message tests", function() {
  beforeEach(function() {
    setUpHTMLFixture();
    $('#txtMessage').val(MSG);
    $('#btnShowMessage').trigger( "click" );
  });
  
  it ("should display the message when button is clicked.", function() {
    expect($('#pMsg')).toHaveText($('#txtMessage').val());
  });
});
 
describe("Hide message tests", function() {
  beforeEach(function() {
    setUpHTMLFixture();
    $('#pMsg').text(MSG);
    $('#btnHideMessage').trigger( "click" );
  });
  
  it ("should remove the message when button is clicked.", function() {
    expect($('#pMsg')).toHaveText("");
  });
});

Here is the complete source of the SpecRunner.html file that we created today:

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Jasmine v2.0 DOM Tests Spec Runner</title>

  <link rel="shortcut icon" type="image/png" href="images/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-core/jasmine.css">

  <script type="text/javascript" src="lib/jasmine-core/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jasmine-html.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/boot.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jquery-1.7.2.js"></script>
  <script type="text/javascript" src="lib/jasmine-core/jasmine-jquery.js"></script>
  
  <script type="text/javascript">
     var MSG = "Hello World!";
     
     function hideMessage() {
       $( "#pMsg" ).html("");
     }
	
     function showMessage() {
       $( "#pMsg" ).html(MSG); 
     }
	 
     function setUpHTMLFixture() {
       setFixtures('<form id="testForm" action="">'
                  +'  <h1>Test Form</h1>'
                  +'  <input type="text" id="txtMessage">'
                  +'  <br>'
                  +'  <button id="btnHideMessage" type="button" onclick="hideMessage()">Hide Message</button>'
                  +'  <button id="btnShowMessage" type="button" onclick="showMessage()">Show Message</button>'
                  +'  <br>'
                  +'  <p id="pMsg"></p>'
                  +'</form>');
      
    }

describe("DOM TESTS:***************", function() { 
  describe("Button Click Event Tests", function() {
    var spyEvent;
	  
    beforeEach(function() {
      setUpHTMLFixture();
    });
      
    it ("should invoke the btnShowMessage click event.", function() {
      spyEvent = spyOnEvent('#btnShowMessage', 'click');
      $('#btnShowMessage').trigger( "click" );
	      
      expect('click').toHaveBeenTriggeredOn('#btnShowMessage');
      expect(spyEvent).toHaveBeenTriggered();
    });
      
    it ("should invoke the btnHideMessage click event.", function() {
      spyEvent = spyOnEvent('#btnHideMessage', 'click');
      $('#btnHideMessage').trigger( "click" );
	      
      expect('click').toHaveBeenTriggeredOn('#btnHideMessage');
      expect(spyEvent).toHaveBeenTriggered();
    });
  });
  	  
  describe("Show message tests", function() {
    beforeEach(function() {
      setUpHTMLFixture();
      $('#txtMessage').val(MSG);
      $('#btnShowMessage').trigger( "click" );
    });
    
    it ("should display the message when button is clicked.", function() {
      expect($('#pMsg')).toHaveText($('#txtMessage').val());
    });
  });
   
  describe("Hide message tests", function() {
    beforeEach(function() {
      setUpHTMLFixture();
      $('#pMsg').text(MSG);
      $('#btnHideMessage').trigger( "click" );
    });
    
    it ("should remove the message when button is clicked.", function() {
      expect($('#pMsg')).toHaveText("");
    });
  });
});
</script>
</head>
<body>
</body>
</html>

Conclusion

Testing more complex events often introduces an asynchronous element, requiring the use of Jasmine 2.0's new done() function. We'll cover how to use it next time.


If you enjoyed this article, please contribute to Rob's rock star aspirations by purchasing one of Rob's cover or original songs from iTunes.com for only 0.99 cents each.

Rob Gravelle resides in Ottawa, Canada, and is the founder of GravelleWebDesign.com. Rob has built systems for Intelligence-related organizations such as Canada Border Services, CSIS as well as for numerous commercial businesses. Email Rob to receive a free estimate on your software project.

In his spare time, Rob has become an accomplished guitar player, and has released several CDs. His band, Ivory Knight, was rated as one Canada's top hard rock and metal groups by Brave Words magazine (issue #92).

Rob uses and recommends MochaHost, which provides Web Hosting at $1.95 per month, 2 LifeTime Free Domains, and 6 Months Free!



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •  
  •  
Thanks for your registration, follow us on our social networks to keep up-to-date