ptg 12.4 Making Get Requests 263 var url = "/url"; ajax.get(url); assertEquals(["GET", url, true], this.xhr.open.args); } }); Much better. Re-running the tests confirm that they now all pass. Moving for- ward, we can add stubs to the fakeXMLHttpRequest object as we see fit, which will make testing ajax.get significantly simpler. 12.4.2.4 Feature Detection and ajax.create ajax.get now relies on the ajax.create method, which is not available in the case that thebrowserdoes not supportthe XMLHttpRequest object. To make sure we don’t provide an ajax.get method that has no way of retrieving a transport, we will define this method conditionally as well. Listing 12.27 shows the required test. Listing 12.27 Bailing out if ajax.create is not available (function () { var ajax = tddjs.namespace("ajax"); if (!ajax.create) { return; } function get(url) { /* */ } ajax.get = get; }()); With this test in place, clients using the ajax.get method can add a similar test to check for its existence before using it. Layering feature detection this way makes it manageable to decide what features are available in a given environment. 12.4.3 Handling State Changes Next up,the XMLHttpRequest object needs tohave itsonreadystatechange handler set to a function, as Listing 12.28 shows. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 264 Abstracting Browser Differences: Ajax Listing 12.28 Verifying that the ready state handler is assigned "test should add onreadystatechange handler": function () { ajax.get("/url"); assertFunction(this.xhr.onreadystatechange); } As expected, the test fails because xhr.onreadystatechange is unde- fined. We can assign an empty function for now, as Listing 12.29 shows. Listing 12.29 Assigning an empty onreadystatechange handler function get(url) { /* */ transport.onreadystatechange = function () {}; } To kick off the request, we need to call the send method. This means that we need to add a stubbed send method to fakeXMLHttpRequest and assert that it was called. Listing 12.30 shows the updated object. Listing 12.30 Adding a stub send method var fakeXMLHttpRequest = { open: stubFn(), send: stubFn() }; Listing 12.31 expects the send method to be called by ajax.get. Listing 12.31 Expecting get to call send TestCase("GetRequestTest", { /* */ "test should call send": function () { ajax.get("/url"); assert(xhr.send.called); } }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.4 Making Get Requests 265 Implementation, shown in Listing 12.32, is once again a one-liner. Listing 12.32 Calling send function get(url) { /* */ transport.send(); } All lights are green once again. Notice how stubXMLHttpRequest is already paying off. We didn’t need to update any of the other stubbed tests even when we called a new method on the XMLHttpRequest object, seeing as they all get it from the same source. 12.4.4 Handling the State Changes ajax.get is now complete in an extremely minimalistic way. It sure ain’t done, but it could be used to send a GET request to the server. We will turn our focus to the onreadystatechange handler in order to allow users of the API to subscribe to the success and failure events. The state change handler is called as the request progresses. Typically, it will be called once for each of these 4 states (from the W3C XMLHttpRequest spec draft. Note that these states have other names in some implementations): 1. OPENED, open has been called, setRequestHeader and send may be called. 2. HEADERS RECEIVED, send has been called, and headers and status are available. 3. LOADING, Downloading; responseText holds partial data. 4. DONE, The operation is complete. For larger responses, the handler is called with the loading state several times as chunks arrive. 12.4.4.1 Testing for Success To reach our initial goal, we really only care about when the request is done. When it is done we check the request’s HTTP status code to determine if it was successful. We can start by testing the usual case of success: ready state 4 and status 200. Listing 12.33 shows the test. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 266 Abstracting Browser Differences: Ajax Listing 12.33 Testing ready state handler with successful request TestCase("ReadyStateHandlerTest", { setUp: function () { this.ajaxCreate = ajax.create; this.xhr = Object.create(fakeXMLHttpRequest); ajax.create = stubFn(this.xhr); }, tearDown: function () { ajax.create = this.ajaxCreate; }, "test should call success handler for status 200": function () { this.xhr.readyState = 4; this.xhr.status = 200; var success = stubFn(); ajax.get("/url", { success: success }); this.xhr.onreadystatechange(); assert(success.called); } }); Because we are going to need quite a few tests targeting the onreadystate- change handler, we create a new test case. This way it’s implicit that test names describe expectations on this particular function, allowing us to skip prefixing every test with “onreadystatechange handler should.” It also allows us to run these tests alone should we run into trouble and need even tighter focus. To pass this test we need to do a few things. First, ajax.get needs to accept an object of options; currently the only supported option is a success callback. Then we need to actually add a body to that ready state function we added in the previous section. The implementation can be viewed in Listing 12.34. Listing 12.34 Accepting and calling the success callback (function () { var ajax = tddjs.namespace("ajax"); function requestComplete(transport, options) { if (transport.status == 200) { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.4 Making Get Requests 267 options.success(transport); } } function get(url, options) { if (typeof url != "string") { throw new TypeError("URL should be string"); } var transport = ajax.create(); transport.open("GET", url, true); transport.onreadystatechange = function () { if (transport.readyState == 4) { requestComplete(transport, options); } }; transport.send(); } ajax.get = get; }()); In order to avoid having the ajax.get method encompass everything but the kitchen sink, handling the completed request was extracted into a separate function. This forced the anonymous closure around the implementation, keeping the helper function local. Finally, with an enclosing scope we could “import” the tddjs.ajax namespace locally here, too. Wow, that was quite a bit of work. Tests were run in between each operation, I promise. The important thing is that the tests all run with this implementation. You may wonder why we extracted requestComplete and not the whole ready state handler. In order to allow the handler access to the options object, we would have had to either bind the handler to it or call the function from inside an anonymous function assigned to onreadystatechange. In either case we would have ended up with two function calls rather than one in browsers without a native bind implementation. For requests incurring a large response, the handler will be called many times (with readyState 3), and the duplicated function calls would have added unnecessary overhead. Now then, what do you suppose would happen if the readystatechange handler is called and we didn’t provide a success callback? Listing 12.35 intends to find out. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 268 Abstracting Browser Differences: Ajax Listing 12.35 Coping with successful requests and no callback "test should not throw error without success handler": function () { this.xhr.readyState = 4; this.xhr.status = 200; ajax.get("/url"); assertNoException(function () { this.xhr.onreadystatechange(); }.bind(this)); } Because we now need to access this.xhr inside the callback to assert- NoException, we bind the callback. For this to work reliably across browsers, save the Function.prototype.bind implementation from Chapter 6, Applied Functions and Closures, in lib/function.js. As expected, this test fails. ajax.get blindly assumes both an options object and the success callback. To pass this test we need to make the code more defensive, as in Listing 12.36. Listing 12.36 Taking care with optional arguments function requestComplete(transport, options) { if (transport.status == 200) { if (typeof options.success == "function") { options.success(transport); } } } function get(url, options) { /* */ options = options || {}; var transport = ajax.create(); /* */ }; With this safety net in place, the test passes. The success handler does not need to verify the existence of the options argument. As an internal function we have absolute control over how it is called, and the conditional assignment in ajax.get guarantees it is not null or undefined. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.5 Using the Ajax API 269 12.5 Using the Ajax API As crude as it is, tddjs.ajax.get is now complete enough that we expect it to be functional. We have built it step-by-step in small iterations from the ground up, and have covered the basic happy path. It’s time to take it for a spin, to verify that it actually runs in the real world. 12.5.1 The Integration Test To use the API weneed an HTMLpage to hostthe test. The test page will makea sim- ple request for another HTML page and add the results to the DOM. The test page can be viewed in Listing 12.37 with the test script, successful _ get _ test.js, following in Listing 12.38. Listing 12.37 Test HTML document <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <title>Ajax Test</title> </head> <body onload="startSuccessfulGetTest()"> <h1>Ajax Test</h1> <div id="output"></div> <script type="text/javascript" src=" /lib/tdd.js"></script> <script type="text/javascript" src=" /src/ajax.js"></script> <script type="text/javascript" src=" /src/request.js"></script> <script type="text/javascript" src="successful_get_test.js"></script> </body> </html> Listing 12.38 The integration test script function startSuccessfulGetTest() { var output = document.getElementById("output"); if (!output) { return; } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 270 Abstracting Browser Differences: Ajax function log(text) { if (output && typeof output.innerHTML != "undefined") { output.innerHTML += text; } else { document.write(text); } } try { if (tddjs.ajax && tddjs.get) { var id = new Date().getTime(); tddjs.ajax.get("fragment.html?id=" + id, { success: function (xhr) { log(xhr.responseText); } }); } else { log("Browser does not support tddjs.ajax.get"); } } catch (e) { log("An exception occured: " + e.message); } } As you can see from the test script’s log function, I intend to run the tests in some ancient browsers. The fragment being requested can be seen in Listing 12.39. Listing 12.39 HTML fragment to be loaded asynchronously <h1>Remote page</h1> <p> Hello, I am an HTML fragment and I was fetched using <code>XMLHttpRequest</code> </p> 12.5.2 Test Results Running the tests is mostly a pleasurable experience even though it does teach us a few things about the code. Perhaps most surprisingly, the test is unsuccess- ful in Firefox up until version 3.0.x. Even though the Mozilla Developer Center documentation states that send takes an optional body argument, Firefox 3.0.x and previous versions will in fact throw an exception if send is called without an argument. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12.5 Using the Ajax API 271 Having discovered a deficiency in the wild, our immediate reaction as TDD-ers is to capture it in a test. Capturing the bug by verifying that our code handles the exception is all fine, but does not help Firefox <= 3.0.x get the request through. A better solution is to assert that send is called with an argument. Seeing that GET requests never have a request body, we simply pass it null. The test goes in the GetRequestTest test case and can be seen in Listing 12.40. Listing 12.40 Asserting that send is called with an argument "test should pass null as argument to send": function () { ajax.get("/url"); assertNull(this.xhr.send.args[0]); } The test fails, so Listing 12.41 updates ajax.get to pass null directly to send. Listing 12.41 Passing null to send function get(url, options) { /* */ transport.send(null); } Our tests are back to a healthy green, and now the integration test runs smoothly on Firefox as well. In fact, it now runs on all Firefox versions, includ- ing back when it was called Firebird (0.7). Other browsers cope fine too, for in- stance Internet Explorer versions 5 and up run the test successfully. The code was tested on a wide variety of new and old browsers. All of them either com- pleted the test successfully or gracefully printed that “Browser does not support tddjs.ajax.get.” 12.5.3 Subtle Trouble Ahead There is one more problem with the code as is, if not as obvious as the pre- vious obstacle. The XMLHttpRequest object and the function assigned to its onreadystatechange property creates a circular reference that causes mem- ory leaks in Internet Explorer. To see this in effect, create another test page like the previous one, only make 1,000 requests. Watch Internet Explorer’s memory usage in the Windows task manager. It should skyrocket, and what’s worse is that From the Library of WoweBook.Com Download from www.eBookTM.com ptg 272 Abstracting Browser Differences: Ajax it will stay high even when you leave the page. This is a serious problem, but one that is luckily easy to fix; simply break the circular reference either by removing the onreadystatechange handler or null the request object (thus removing it from the handler’s scope) once the request is finished. We will use the test case to ensure that this issue is handled. Although nulling the transport is simple, we cannot test it, because it’s a local value. We’ll clear the ready state handler instead. Clearing the handler can be done in a few ways; setting it to null or using the delete operator quickly comes to mind. Enter our old friend Internet Explorer. Using delete will not work in IE; it returns false, indicating that the property was not successfully deleted. Setting the property to null (or any non-function value) throws an exception. The solution is to set the property to a function that does not include the request object in its scope. We can achieve this by creating a tddjs.noop function that is known to have a “clean” scope chain. Using a function available outside the implementation handily lends itself to testing as well, as Listing 12.42 shows. Listing 12.42 Asserting that the circular reference is broken "test should reset onreadystatechange when complete": function () { this.xhr.readyState = 4; ajax.get("/url"); this.xhr.onreadystatechange(); assertSame(tddjs.noop, this.xhr.onreadystatechange); } As expected, this test fails. Implementing it is as simple as Listing 12.43. Listing 12.43 Breaking the circular reference tddjs.noop = function () {}; (function () { /* */ function get(url, options) { /* */ transport.onreadystatechange = function () { if (transport.readyState == 4) { From the Library of WoweBook.Com Download from www.eBookTM.com . PUBLIC " ;-/ /W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html;. type="text /javascript& quot; src=" /lib/tdd.js"></script> <script type="text /javascript& quot; src=" /src/ajax.js"></script> <script type="text /javascript& quot; src=". a few tests targeting the onreadystate- change handler, we create a new test case. This way it’s implicit that test names describe expectations on this particular function, allowing us to skip