The IFRAME Technique for Testing Web Pages

By Rob Gravelle

Just recently, I wrote about Jasmine 2.0 in my Testing DOM Events Using jQuery and Jasmine 2.0 article. It described how to utilize spies to verify that a given event was triggered and that it resulted in the expected action. While adequate for simple events such as a message being displayed and the like, spies by themselves aren't enough to test longer asynchronous processes. For those, you need to use the new done() method. It replaces the runs() and waitsFor() methods of earlier jasmine versions to make it more similar to other testing frameworks. In today's article, we're going to learn how to use Jasmine 2.0's done() method to test DOM events without polluting the test page with test code.

Using the done() Function

The done() function is always passed to the beforeEach(), afterEach(), and it() test methods as an argument; whether you make use of it or not is up to you. To use it, include the done argument to the method and the call it after all of the processing is complete. This will usually be in the success callback function of Ajax calls and the pertinent event listener of DOM events. To illustrate, here is a unit test for menu retrieval. The first beforeEach() does not include the done() function because there is no asynchronous processing taking place within it. However, the beforeEach() of the nested describe ("when retrieved by name") does because the menus' getMenuByName() function is asynchronous. The done() call is made within the success() callback function to instruct jasmine that beforeEach() has terminated and it is now safe to continue with the it() function. There are no asynchronous events in the it() function, so the done() function is not utilized, although we could include it if we needed to.

describe("Menu Tests", function() {
  var menus;
  
  beforeEach(function() { 
    menus = new Menus();
    menus.add(new Menu({
        name:   'dinner',
        season: 'Fall'
    }));
  });
  
  describe("when retrieved by name", function() { 
    var menu;
    
    beforeEach(function(done) {
      menu = menus.getMenuByName('dinner', {
        success: function () {
          done();
        }
      });
    });
  
    it("should have a valid season", function() { 
      expect(menu.season).toEqual("Fall");
    });
  });
});

The Test Page

The following jQuery-enabled web page contains two DIVs and a button that fetches the latest restaurant menus from the server. After the page loads, the local getNewMenus() function is bound to the button's onclick event. Inside the event handler, an Ajax get() call retrieves the restaurant data from the server. Presumably, a server-side process would handle the data retrieval, but for the sake of simplicity, this call simply reads a text file and appends the HTML contents into the content-area DIV via the successCallback() method. Our unit test will cover the end-to-end process from the button click to the new restaurant being inserted into the DOM. Since there is no way of knowing how long that will take, it is an ideal candidate for the done() function.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Target Page</title>
<style>
  #results {
  	font-size: 1.2rem; 
  	margin: 10px; 
  	height: 50px;
  }	
  
  #content-area {
    background-color: #FFFFFF;
    height: 100px;
    padding-left: 5px;
    width: 200px;
  }
</style>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript">
function successCallback(data) { 
  $( "#content-area" ).append( data );
}

function failCallback(data) { 
  $( "#results" ).text( "Load new menus failed." );  
}

function getNewMenus() {
  //new menus.txt contains one html-formatted restaurant name and link
  //<p><a href="/restaurant/Robs_Bistro/#title">Robs Bistro</a></p>
  $.get( "new menus.txt", successCallback).fail(failCallback);
}

jQuery(document).ready(function($) {
  $('#btnGetNewMenus').click(getNewMenus);
});
</script>
</head>
<body>
<div id="results"></div>
<div id="content-area"></div>
<button id="btnGetNewMenus">Get New Menus</button>
</body>
</html>

Setting Up the Test Environment

It's easy enough to reference a script file for testing, but the DOM is another matter. One way to test it without altering the original page is to use an IFRAME. Here is a skeleton of the page that will contain our unit tests. It contains all of the script references, IFRAME, and an empty JavaScript tag to hold our test code. The IFRAME src is set to "about:blank" so that we can control the loading of the test page in code.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM Tests using Jasmine v2.0</title>
  <link rel="shortcut icon" type="image/png" href="images/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="scripts/jasmine-master/lib/jasmine-core/jasmine.css">

  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine-html.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/boot.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jquery-1.7.2.js"></script>
  <script type="text/javascript" src="scripts/jasmine-master/lib/jasmine-core/jasmine-jquery.js"></script>
  
  <script type="text/javascript">
    //tests go here…
  </script>
</head>
<body>
<iframe id="testFrame" src="about:blank" style="" height="500" width="800"></iframe>
</body>
</html>

Like the main page, the IFRAME also fires the onload event. That's where we can hook in some initial tests to confirm that the page was loaded correctly. The jQuery one() event handler is similar to the more familiar on(), but it's guaranteed to execute at most once per element per event type - perfect for one-time initialization such as this. After setting the handler, we are ready to load the target test page. There are a few ways to do this, but setting the IFRAME's src attribute is a good one.

  var testPage = '/DynamicHTML5WebProject/targetPage.html',
      testFrame;
      
  jQuery(document).ready(function($) {
    testFrame = $('#testFrame');
    testFrame.one('load', function() {      
      describe("Verify Test Environment", function() {
  
      it ("should load the target test page.", function() {
        expect(testFrame).toExist();
        expect(testFrame).toBeInDOM();
        expect(testFrame).toHaveAttr('src', testPage);
      });
    });
    testFrame.attr('src', testPage);
  });

The Test Code

Immediately following the jQuery document ready() closing tag is where we'll append our tests. As per the usual jasmine convention, the beforeEach() invokes the target process and then the it() function verifies expectations. The IFRAME document's DOM is accessible via the jQuery iFrame.contents() function. From there, we can call find() with the button's ID to reference it. To invoke an event, jQuery provides the trigger(eventName) method.

There is only one thing missing from the beforeEach() and that's the call to done()! As it stands, the it() function won't know when to begin, which will result in a timeout. It belongs at the end of the Ajax get() success callback function because it's really the only place in the button click's chain of events that we can be sure that the DOM has been updated. JavaScript is versatile enough that you could alter the original function on the fly and include the done() call on the last line. However, there's an easier way. If you're familiar with Aspect Oriented Programming (AOP), you are no doubt aware that it is possible to wrap a proxy object around the original function so that it includes the extra code. The wrapper basically contains a call to the original function, followed by the done() invocation.

   
    jQuery(document).ready(function($) {
      //...
      testFrame.attr('src', testPage);
    });  
    
    describe("Test GetNewMenus()", function() {
      beforeEach(function(done) {  
        testFrame[0].contentWindow.successCallback = (function(originalFunction) {
          return function(data) {
            originalFunction(data);
            done();
          };
        })(testFrame[0].contentWindow.successCallback);
        testFrame.contents().find("#btnGetNewMenus").trigger('click');
      });
        
      it ("should fetch the new restaurant meta data from the server.", function() {
        var listEntry = testFrame.contents().find("#content-area > p").last();
        expect(listEntry).toExist();
        expect(listEntry).toBeInDOM();
        expect(listEntry).toContainElement('a');
	      expect(listEntry).toContainText("Robs Bistro");
	      expect(listEntry.children('a:first')).toHaveAttr('href', '/restaurant/Robs_Bistro/#title');
      });
    });
    testFrame.attr('src', testPage);
  });

The it() function contains several expectation matchers form the excellent jasmine-jquery library. We used it in the last article as well. Using it, we can verify that the listEntry exists, is in the DOM, contains an <a> element that includes the restaurant name and the expected URL.

Here is what a successful test run looks like in Firefox. Note that the IFRAME size has been reduced.

test_results (11K)

Conclusion

Jasmine is one of the most successful JavaScript testing libraries out there, and for good reason. It's equally well suited for GUI testing as well as background business processing. There were some rumblings about the use of the runs() and waitsFor() functions, but with the release of 2.0, those complaints should be put to rest.



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •