ptg 15.4 The Message List 413 Listing 15.46 Expecting setModel to observe the “message” channel TestCase("MessageListControllerSetModelTest", { "test should observe model's message channel": function () { var controller = Object.create(listController); var model = { observe: stubFn() }; controller.setModel(model); assert(model.observe.called); assertEquals("message", model.observe.args[0]); assertFunction(model.observe.args[1]); } }); The test fails, and Listing 15.47 helps passing it by making the call to observe. Listing 15.47 Calling observe function setModel(model) { model.observe("message", function () {}); } Next, we’ll expect the handler to be a bound addMessage method, much like we did with the DOM event handler in the user form controller. Listing 15.48 shows the test. Listing 15.48 Expecting a bound addMessage as “message” handler TestCase("MessageListControllerSetModelTest", { setUp: function () { this.controller = Object.create(listController); this.model = { observe: stubFn() }; }, /* */ "test should observe with bound addMessage": function () { var stub = this.controller.addMessage = stubFn(); this.controller.setModel(this.model); this.model.observe.args[1](); assert(stub.called); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 414 TDD and DOM Manipulation: The Chat Client assertSame(this.controller, stub.thisValue); } }); I jumped the gun slightly on this one, immediately recognizing that a setUp was required to avoid duplicating the test setup code. The test should look eerily familiar because it basically mimics the test we wrote to verify that userFormController observed the submit event with a bound handleSubmit method. Listing 15.49 adds the correct handler to model.observe. What are your expectations as to the result of running the tests? Listing 15.49 Observing the “message” channel with a bound method function setModel(model) { model.observe("message", this.addMessage.bind(this)); } If you expected the test to pass, but the previous test to fail, then you’re abso- lutely right. As before, we need to add the method we’re binding to the controller, to keep tests that aren’t stubbing it from failing. Listing 15.50 adds the method. Listing 15.50 Adding an empty addMessage /* */ function addMessage(message) {} chat.messageListController = { setModel: setModel, addMessage: addMessage }; Before we can move on to test the addMessage method, we need to add a view, because addMessage’s main task is to build DOM elements to inject into it. As before, we’re turning a blind eye to everything but the happy path. What happens if someone calls setModel without an object? Or with an object that does not support observe? Write a few tests, and update the implementation as you find necessary. 15.4.2 Setting the View With the experience we gained while developing the user form controller, we will use DOM elements in place of fake objects right from the start while developing setView for the list controller. Listing 15.51 verifies that the method adds the “js-chat” class to the view element. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.4 The Message List 415 Listing 15.51 Expecting setView to set the element’s class function messageListControllerSetUp() { /*:DOC element = <dl></dl> */ this.controller = Object.create(listController); this.model = { observe: stubFn() }; } TestCase("MessageListControllerSetModelTest", { setUp: messageListControllerSetUp, /* */ }); TestCase("MessageListControllerSetViewTest", { setUp: messageListControllerSetUp, "test should set class to js-chat": function () { this.controller.setView(this.element); assertClassName("js-chat", this.element); } }); We’ve danced the extract setup dance enough times now that hopefully the above listing should not be too frightening. Even though parts of the TDD process do become predictable after awhile, it’s important to stick to the rhythm. No matter how obvious some feature may seem, we should be extremely careful about adding it until we can prove we really need it. Remember, You Ain’t Gonna Need It. Keeping to the rhythm ensures neither production code nor tests are any more complicated than what they need to be. The test fails because the setView method does not exist. Listing 15.52 adds it and passes the test in one fell swoop. Listing 15.52 Adding a compliant setView method function setView(element) { element.className = "js-chat"; } chat.messageListController = { setModel: setModel, setView: setView, addMessage: addMessage }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 416 TDD and DOM Manipulation: The Chat Client That’s it for now. We’ll need the method to actually store the view as well, but preferably without poking at its implementation. Also, currently there is no need to store it, at least not until we need to use it in another context. 15.4.3 Adding Messages Onwards to the heart and soul of the controller; receiving messages, building DOM elements for them, and injecting them into the view. The first thing we will test for is that a dt element containing the user prefixed with an “@” is added to the definition list, as Listing 15.53 shows. Listing 15.53 Expecting the user to be injected into the DOM in a dt element TestCase("MessageListControllerAddMessageTest", { setUp: messageListControllerSetUp, "test should add dt element with @user": function () { this.controller.setModel(this.model); this.controller.setView(this.element); this.controller.addMessage({ user: "Eric", message: "We are trapper keeper" }); var dts = this.element.getElementsByTagName("dt"); assertEquals(1, dts.length); assertEquals("@Eric", dts[0].innerHTML); } }); The test adds a message and then expects the definition list to have gained a dt element. To pass the test we need to build an element and append it to the view, as Listing 15.54 shows. Listing 15.54 Adding the user to the list function addMessage(message) { var user = document.createElement("dt"); user.innerHTML = "@" + message.user; this.view.appendChild(user); } Boom! Test fails; this.view is undefined. There we go, a documented need for the view to be kept in a property. Listing 15.55 fixes setView to store a reference to the element. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.4 The Message List 417 Listing 15.55 Storing a reference to the view element function setView(element) { element.className = "js-chat"; this.view = element; } With a reference to the view in place, all the tests pass. That leaves the message, which should be added to the DOM as well. Listing 15.56 shows the test. Listing 15.56 Expecting the message to be added to the DOM TestCase("MessageListControllerAddMessageTest", { setUp: function () { messageListControllerSetUp.call(this); this.controller.setModel(this.model); this.controller.setView(this.element); }, /* */ "test should add dd element with message": function () { this.controller.addMessage({ user: "Theodore", message: "We are one" }); var dds = this.element.getElementsByTagName("dd"); assertEquals(1, dds.length); assertEquals("We are one", dds[0].innerHTML); } }); Again, some test setup code was immediately elevated to the setUp method, to keep the goal of the test obvious. To pass this test, we basically just need to repeat the three lines from before, changing the text content and tag name. Listing 15.57 has the lowdown. Listing 15.57 Adding the message as a dd element function addMessage(message) { /* */ var msg = document.createElement("dd"); msg.innerHTML = message.message; this.view.appendChild(msg); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 418 TDD and DOM Manipulation: The Chat Client The server currently does not filter messages in any way. To avoid users ef- fortlessly hijacking the chat client, we will add one test that expects any messages including HTML to be escaped, as seen in Listing 15.58. Listing 15.58 Expecting basic cross site scripting protection "test should escape HTML in messages": function () { this.controller.addMessage({ user: "Dr. Evil", message: "<script>window.alert('p4wned!');</script>" }); var expected = "<script>window.alert('p4wned!');" + "</script>"; var dd = this.element.getElementsByTagName("dd")[1]; assertEquals(expected, dd.innerHTML); } The test fails; no one is stopping Dr. Evil from having his way with the chat client. Listing 15.59 adds basic protection against script injection. Listing 15.59 Adding basic XSS protection function addMessage(message) { /* */ msg.innerHTML = message.message.replace(/</g, "<"); this.view.appendChild(msg); } 15.4.4 Repeated Messages From Same User Before we get going on the message form controller, we will add one more test. If we receive multiple messages in a row from the same user, we will expect the controller to not repeat the user. In other words, if two consecutive messages originate from the same user, we will not add a second dt element. Listing 15.60 tests for this feature by adding two messages and expecting only one dt element. Listing 15.60 Expecting controller not to repeat dt elements "test should not repeat same user dt's": function () { this.controller.addMessage({ user: "Kyle", message: "One-two-three not it!" }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.4 The Message List 419 this.controller.addMessage({ user:"Kyle", message:":)" }); var dts = this.element.getElementsByTagName("dt"); var dds = this.element.getElementsByTagName("dd"); assertEquals(1, dts.length); assertEquals(2, dds.length); } Unsurprisingly, the test fails. To pass it, we need the controller to keep track of the previous user. This can be done by simply keeping a property with the last seen user. Listing 15.61 shows the updated addMessage method. Listing 15.61 Keeping track of the previous user function addMessage(message) { if (this.prevUser != message.user) { var user = document.createElement("dt"); user.innerHTML = "@" + message.user; this.view.appendChild(user); this.prevUser = message.user; } /* */ } Note that non-existent properties resolve to undefined, which will never be equal to the current user, meaning that we don’t need to initialize the property. The first time a message is received, the prevUser property will not match the user, so a dt is added. From here on, only messages from new users will cause another dt element to be created and appended. Also note that node lists, as those returned by getElementsByTagName are live objects, meaning that they always reflect the current state of the DOM. As we are now accessing both the collection of dt and dd elements from both tests, we could fetch those lists in setUp as well to avoid duplicating them. I’ll leave updating the tests as an exercise. Another exercise is to highlight any message directed at the current user, by marking the dd element with a class name. Remember, the current user is available through this.model.currentUser, and “directed at” is defined as “message starts with @user:”. Good luck! From the Library of WoweBook.Com Download from www.eBookTM.com ptg 420 TDD and DOM Manipulation: The Chat Client 15.4.5 Feature Tests The message list controller can only work correctly if it is run in an environment with basic DOM support. Listing 15.62 shows the controller with its required feature tests. Listing 15.62 Feature tests for messageListController (function () { if (typeof tddjs == "undefined" || typeof document == "undefined" || !document.createElement) { return; } var element = document.createElement("dl"); if (!element.appendChild || typeof element.innerHTML != "string") { return; } element = null; /* */ }()); 15.4.6 Trying it Out As the controller is now functional, we will update chapp to initialize it once the user has entered his name. First, we need a few new dependencies. Copy the following files from Chapter 13, Streaming Data with Ajax and Comet, into public/js: • json2.js • url params.js • ajax.js • request.js • poller.js • comet client.js Also copy over the message _ list _ controller.js file, and finally add script elements to the index.html below the previous includes in the order listed above. Make sure the js/chat _ client.js file stays included last. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.4 The Message List 421 Add an empty dl element to index.html and assign it id="messages". Then update the chat _ client.js file as seen in Listing 15.63. Listing 15.63 Updated bootstrap script (function () { if (typeof tddjs == "undefined" || typeof document == "undefined") { return; } var c = tddjs.namespace("chat"); if (!document.getElementById || !tddjs || !c.userFormController || !c.messageListController) { alert("Browser is not supported"); return; } var model = Object.create(tddjs.ajax.cometClient); model.url = "/comet"; /* */ userController.observe("user", function (user) { var messages = document.getElementById("messages"); var messagesController = Object.create(c.messageListController); messagesController.setModel(model); messagesController.setView(messages); model.connect(); }); }()); Start the server again, and repeat the exercise from Listing 14.27 in Chapter 14, Server-Side JavaScript with Node.js. After posting a message using curl, it should immediately appear in your browser. If you post enough messages, you’ll notice that the document eventually gains a scroll and that messages appear below the fold. That clearly isn’t very helpful, so we’ll make a note of it and get right on it as we add some finishing touches toward the end of the chapter. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 422 TDD and DOM Manipulation: The Chat Client 15.5 The Message Form The message form allows users to post messages. The steps required to test and implement it are going to be very similar to the user form controller we created previously: it needs a form element as its view; it will handle the form’s submit event through its handleSubmit method; and finally it will publish the message as an event on the model object, which passes it to the server. 15.5.1 Setting up the Test The first thing we need to do is to set up the test case and expect the controller object to exist. Listing 15.64 shows the initial test case. Listing 15.64 Setting up the messageFormController test case (function () { var messageController = tddjs.chat.messageFormController; TestCase("FormControllerTestCase", { "test should be object": function () { assertObject(messageController); } }); }()); Running the test prompts us to define the object with a big fat red “F.” Listing 15.65 does the grunt work. Listing 15.65 Defining the message form controller (function () { var chat = tddjs.namespace("chat"); chat.messageFormController = {}; }()); 15.5.2 Setting the View Just like the user form controller, this controller needs to add the “js-chat” class name to its view and observe the “submit” event with the handleSubmit method bound to the controller. In fact, setting the view for the message form controller should work exactly like the one we previously wrote. We’ll try to be slightly smarter than to simply repeat the entire process; it seems obvious that the two form controllers should share parts of their implementation. From the Library of WoweBook.Com Download from www.eBookTM.com . messageListControllerSetUp, "test should set class to js-chat": function () { this.controller.setView(this.element); assertClassName("js-chat", this.element); } }); We’ve danced the. dt's": function () { this.controller.addMessage({ user: "Kyle", message: "One-two-three not it!" }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 15.4. = Object.create(c.messageListController); messagesController.setModel(model); messagesController.setView(messages); model.connect(); }); }()); Start the server again, and repeat the exercise from Listing 14.27 in Chapter 14, Server-Side JavaScript with Node.js. After posting a message using curl, it should immediately appear in